Date: 2025-12-18
Issue: 20-30 second delay for incoming call notifications on cold start
| Event | First Call | Second Call | Difference |
|---|---|---|---|
| FCM Message Received | 20:56:14.718 | 21:03:49.690 | - |
| React Native Boot Complete | 20:56:19.307 | (already running) | - |
| CallKeep Display Called | 20:56:32.982 | 21:03:49.874 | - |
| System Call UI Shows | 20:56:34.577 | 21:03:50.904 | - |
| Total Delay | ~18-20 seconds | ~1 second | 17-19s faster |
When the app is killed, React Native must:
- ⏱️ Launch process -
~1s - ⏱️ Load native libraries (libreactnative.so, libhermes, etc.) -
~2s - ⏱️ Initialize React Native (Hermes JS engine, bridge) -
~2-3s - ⏱️ Load JS bundle from Metro (dev mode only!) -
~1.5s - ⏱️ Execute headless JS (Firebase handler setup) -
~10-12s - ⏱️ Display call -
~1s
Total: ~18-20 seconds
The app process is still alive from the first call:
- React Native is already initialized
- JS bundle already loaded
- Firebase handlers already registered
- Only needs to execute the message handler
Total: ~1 second
The smoking gun from logs:
20:56:17.828 unknown:BridgelessReact ReactHost{0}.loadJSBundleFromMetro()
In development mode, React Native fetches the JS bundle from Metro bundler over the network (~1.5s delay). This doesn't happen in production builds where the bundle is embedded in the APK.
Error from logs:
20:56:33.297 RNCallKeep
Can't start foreground service: android.app.MissingForegroundServiceTypeException:
Starting FGS without a type targetSDK=36
App targets Android 14 (SDK 36), which requires declaring a foreground service type.
Build a release APK to see real-world performance:
cd android
./gradlew assembleReleaseExpected delay should drop to 5-8 seconds (still slow, but much better than dev).
File: android/app/src/main/AndroidManifest.xml
<service
android:name="io.wazo.callkeep.VoiceConnectionService"
android:exported="true"
android:foregroundServiceType="phoneCall|microphone"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>Also add permissions:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />This fixes the error:
Can't start foreground service: MissingForegroundServiceTypeException
A. Simplify Headless Task Initialization
Your headless task is doing too much on cold start. Reduce work:
// ❌ BAD: Loading entire app state
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
await initializeEverything(); // Don't do this!
await handleIncomingCall(remoteMessage);
});
// ✅ GOOD: Minimal work
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
if (remoteMessage.data?.type === 'incoming_call') {
// Only do what's needed to show the call
await RNCallKeep.displayIncomingCall(/* ... */);
}
});B. Use Android Native Implementation (BEST)
Move call display logic to native Android to bypass React Native entirely.
Create android/app/src/main/java/com/neonmobile/app/MyFcmService.java:
package com.neonmobile.app;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import io.wazo.callkeep.RNCallKeepModule;
import java.util.Map;
public class MyFcmService extends FirebaseMessagingService {
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
Map<String, String> data = remoteMessage.getData();
if ("incoming_call".equals(data.get("type"))) {
// Display call IMMEDIATELY without waiting for React Native
RNCallKeepModule.displayIncomingCall(
data.get("callId"),
data.get("callerName"),
data.get("callerPhone"),
false
);
}
}
}Register in AndroidManifest.xml:
<service android:name=".MyFcmService" android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>This bypasses React Native entirely - call shows in <1 second even on cold start.
Use a persistent background service to keep the app process warm:
// android/app/src/main/java/com/neonmobile/app/KeepAliveService.kt
class KeepAliveService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(1, buildNotification())
return START_STICKY
}
}Trade-off: Uses ~50-100MB RAM permanently. Not ideal for battery life.
- Right Now: Test with release build - this should cut delay to ~5-8s
- Quick Win: Add foreground service type declaration (fixes Android 14 warning)
- Medium-term: Move FCM handling to native code (gets you to <1s consistently)
- Long-term: Consider using Android's system push (eliminates FCM entirely)
| Build Type | First Call Delay | Subsequent Calls |
|---|---|---|
| Dev (current) | 18-20s | 1s |
| Production APK | 5-8s | 1s |
| + Native FCM handler | <1s | <1s |
2025-12-18 20:56:14.718 ActivityManager Start proc com.neonmobile.app.dev
2025-12-18 20:56:17.828 BridgelessReact loadJSBundleFromMetro()
2025-12-18 20:56:32.982 RNCallKeep displayIncomingCall
2025-12-18 20:56:34.577 Dialer Incoming call via Neon (Dev)
2025-12-18 21:03:49.690 RNFirebaseMsgReceiver broadcast received
2025-12-18 21:03:49.874 RNCallKeep displayIncomingCall
2025-12-18 21:03:50.904 TelecomFramework onTrackedByNonUiService
20:56:33.297 RNCallKeep Can't start foreground service:
android.app.MissingForegroundServiceTypeException:
Starting FGS without a type targetSDK=36
The cleanest solution for production-quality VoIP is implementing the native FCM handler. This eliminates the React Native cold-start penalty and provides consistent <1s response times for all incoming calls.