Skip to content

Instantly share code, notes, and snippets.

@konecnyna
Created December 19, 2025 02:09
Show Gist options
  • Select an option

  • Save konecnyna/b1ebb8888e15de8db8a5865b0752a5d3 to your computer and use it in GitHub Desktop.

Select an option

Save konecnyna/b1ebb8888e15de8db8a5865b0752a5d3 to your computer and use it in GitHub Desktop.
Neon Mobile App - Incoming Call Notification Delay Analysis (2025-12-18): Analysis of 20-30 second delay for incoming call notifications on cold start

Neon Mobile App - Incoming Call Notification Delay Analysis

Date: 2025-12-18
Issue: 20-30 second delay for incoming call notifications on cold start


Timing Comparison

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

Root Cause Analysis

First Call: Cold Start Penalty

When the app is killed, React Native must:

  1. ⏱️ Launch process - ~1s
  2. ⏱️ Load native libraries (libreactnative.so, libhermes, etc.) - ~2s
  3. ⏱️ Initialize React Native (Hermes JS engine, bridge) - ~2-3s
  4. ⏱️ Load JS bundle from Metro (dev mode only!) - ~1.5s
  5. ⏱️ Execute headless JS (Firebase handler setup) - ~10-12s
  6. ⏱️ Display call - ~1s

Total: ~18-20 seconds

Second Call: Warm Start (Much Faster)

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


Critical Issue: Dev Metro Bundle Loading

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.


Critical Issue: Missing Foreground Service Type (Android 14+)

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.


Solutions (Prioritized)

✅ 1. Test with Production Build (IMMEDIATE)

Build a release APK to see real-world performance:

cd android
./gradlew assembleRelease

Expected delay should drop to 5-8 seconds (still slow, but much better than dev).


✅ 2. Add Foreground Service Type (REQUIRED for Android 14+)

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

✅ 3. Optimize Cold Start (ADVANCED)

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.


✅ 4. Keep Process Alive (WORKAROUND)

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.


Recommended Action Plan

  1. Right Now: Test with release build - this should cut delay to ~5-8s
  2. Quick Win: Add foreground service type declaration (fixes Android 14 warning)
  3. Medium-term: Move FCM handling to native code (gets you to <1s consistently)
  4. Long-term: Consider using Android's system push (eliminates FCM entirely)

Expected Results After Fixes

Build Type First Call Delay Subsequent Calls
Dev (current) 18-20s 1s
Production APK 5-8s 1s
+ Native FCM handler <1s <1s

Key Log Excerpts

First Call (Cold Start - 18-20s delay)

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)

Second Call (Warm Start - 1s delay)

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

Critical Error (Android 14)

20:56:33.297  RNCallKeep  Can't start foreground service: 
android.app.MissingForegroundServiceTypeException: 
Starting FGS without a type  targetSDK=36

Next Steps

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment