Repository navigation
Fix JS timers stalling after app lifecycle transitions on iOS - #58976
almeidagabriel01 wants to merge 1 commit into
Conversation
|
Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
Summary
RCTTiminghandles app lifecycle notifications (UIApplicationWillResignActive,DidEnterBackground,WillEnterForeground,DidBecomeActive) and proximity changes on the main thread, while every other method of the class runs on the thread that creates timers (the JS thread, throughObjCTimerRegistry).Two consequences:
appDidMoveToBackground→didUpdateFrame:nil→scheduleSleepTimer:installs the backgroundNSTimeron the main run loop, inNSDefaultRunLoopModeonly. While the main thread is busy or running another mode — for example during a Face ID prompt on launch — that timer does not fire. The JS thread cannot schedule its own:scheduleSleepTimer:sees a valid_sleepTimerand only moves its fire date. All JS timers (setTimeout,setInterval,requestAnimationFrame) stall._pausedand_inBackgroundare written from two threads without synchronization (stopTimers/startTimerson main vs.didUpdateFrame:on JS).Measured on an iPhone 16 Pro Max (iOS 26, RN 0.86.3, bridgeless, release build), instrumenting RCTTiming during cold launches behind Face ID:
Fix
Forward lifecycle and proximity handling to the timers thread (
-performOnTimingThread:), capturing its run loop when the first timer is created. Before that there is no timer to race with, so the handler runs in place (under@synchronized(self)). All timer state — and the sleep timer — now live on one thread.Changelog:
[IOS] [FIXED] - JS timers stall after app lifecycle transitions (e.g. Face ID prompt) because RCTTiming handled them on the main thread
Test Plan
Measured and validated on RN 0.86.3 (the same diff, applied with patch-package in a production app); this PR ports it to
main, whereRCTTiming.mmlost the bridge paths but keeps the lifecycle handling unchanged.appDidMoveTo*/startTimers/stopTimers/scheduleSleepTimer:/timerDidFireruns on the JS thread; no sleep timer overdue beyond a frame during the inactive phase.