RORK LABJP
PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updatesPLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
Articles/Dev Tools
Dev Tools/2026-06-01Advanced

When Your Android Vitals ANR Rate Climbs: Notes on Keeping the Main Thread Free in React Native

How I traced and fixed a rising Android Vitals ANR rate in a Rork-built React Native app, plus the main-thread rules I rolled out across six apps in production.

React Native234Android47ANRPerformance25Crashlytics12

Premium Article

One morning a Google Play Console alert caught my eye. Out of the six wallpaper apps I run in parallel (such as Ukiyo-e Wallpapers), exactly one had its Android Vitals "ANR rate" climb to 0.61%, crossing Play's bad-behavior threshold of 0.47% that can affect store visibility. Crashes weren't up. The crash-free rate was holding at 99.6%. And yet users were experiencing something like "it freezes, goes black after a while, then closes on its own."

ANR (Application Not Responding) is a separate signal from crashes. For my first few years I watched crashes almost exclusively and never monitored ANRs properly. Across more than 50 million cumulative downloads since I started building apps on my own in 2014, it was only in the last two or three years that I really felt how nasty ANRs are: the feature works correctly, but the experience is broken. This article is the record of how I traced that one app, and the "keep the main thread free" design rules I then spread to all six.

ANR Is Not "Slow JS" — It Is "the UI Thread Stopped Responding"

When people think about React Native performance, many of us reach first for the weight of the JS thread. I thought that way for a long time. But Android's ANR is, strictly speaking, not about the JavaScript thread.

Android records an ANR mainly in these situations: an input event doesn't get a response within about five seconds, a BroadcastReceiver doesn't finish in time, or a Service doesn't complete within its window. In every case, what's being judged is the main thread (the UI thread). In React Native, JS runs on its own thread, but synchronous native module calls, large data crossing the bridge, native initialization at startup, and image decoding all ultimately show up as work on the main thread. When that gets jammed, you get an ANR no matter how light your JS is.

So reducing ANRs is not "make JS faster." It is "find the spots where the main thread is blocked continuously for a long time, and clear them." Shifting my view to this point was the first breakthrough.

One thing worth adding: since Android 11, ANR accounting has widened to include things like delays in Context.startForegroundService, not just broadcasts and input events. A point that's surprisingly easy to miss in a React Native app is the native initialization that third-party SDKs (ads, analytics, push) run at startup. My six apps all include AdMob, so whether the ad SDK's initialization overlaps with synchronous startup work is something I check every time. If you only suspect your own JS, you'll get lost when the cause is on the SDK side.

Pin Down the Location With Crashlytics and Android Vitals

I narrowed down where it was happening by squeezing it from two directions.

One is Google Play Console's Android Vitals. ANRs are grouped into "clusters" with stack traces. For the problem app, roughly 70% of the top cluster originated from nativeRunnable, and they all concentrated in the first few seconds after launch. That alone points to "somewhere in the startup sequence."

The other is Firebase Crashlytics. Lately, when I update an Android app, I have Claude in Chrome read the Crashlytics screen and organize the correlation between ANR cluster stack traces and the version they appeared in. Having it summarize the on-screen tables — "which release did this start growing from," "which devices and OS versions are over-represented" — lets me narrow my hypothesis before I open any code. This time it skewed toward Android 12 and up on lower-end devices, which read as "surfacing on devices where the synchronous startup work is slow."

My grandfather, a temple carpenter, used to say that "working with your hands is a kind of faith," and ANR investigation is exactly that: measuring each suspicious initialization one at a time is far more reliable than staring at logs and guessing.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
How I triaged one app crossing the 0.47% Android Vitals ANR threshold within 72 hours of release
Before/After code that moves synchronous startup work off the main thread, with a measured 0.61%→0.12% ANR drop
The New Architecture pitfall where synchronous TurboModule calls block the main thread, and the rule I use across six apps
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Dev Tools2026-08-31
Three Minutes After the Screen Locked, My expo-audio Ambient Sound Went Silent
Why expo-audio stops playing when the screen locks, diagnosed as three separate layers: build config, audio session, and lock screen integration. The three-minute stop on Android is documented behavior.
Dev Tools2026-07-24
Collapsing Duplicate Requests Into One: A Reference-Counted Single-Flight Layer
When several components fire the same API call at launch, you get a burst of identical requests. Here is a single-flight layer that shares one in-flight promise instead: how to build the key that decides the folding, the trap of handing out a failed promise forever, the trap of one caller's abort cancelling everyone, and the test that keeps it all from regressing, with the real network numbers alongside.
Dev Tools2026-07-10
Adding React Compiler to Expo Let Me Delete 41 Hand-Written memo Calls
I enabled React Compiler on Rork-generated React Native screens and measured the rerender counts with Profiler. Here is how I decided which memo and useCallback calls were safe to delete, how to find the components the compiler bailed out on, and how to catch regressions in CI.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →