Skip to content
<UK/>

Mobile performance lab

16.7 Milliseconds

A phone at 60 Hz gets 16.7 ms to draw each frame. Scroll a 10,000-card feed and see who spends the budget.

Authored model, not a measurement of your device

60 Hz: 90 of 90 frames dropped on screen. Not smooth yet at 60 Hz.

Modelled phone

Refresh rate

Budget per frame: 16.7 ms

React Native: Performance Overview (opens in a new tab)Android Developers: Slow rendering (opens in a new tab)web.dev: Rendering performance (opens in a new tab)

  1. Garden crew · 1 dLost and found: a blue umbrellaCard #1 · 220 reactions
  2. Garden crew · 2 dMorning run: 5 km, every pace welcomeCard #2 · 67 reactions
  3. Building team · 2 dRooftop garden needs volunteers this weekendCard #3 · 80 reactions
  4. Chess club · 10 hPhoto walk along the shore on SaturdayCard #4 · 364 reactions
  5. Garden crew · 2 dSoup night: bring your own bowlCard #5 · 157 reactions
  6. Neighbours · 1 dMorning run: 5 km, every pace welcomeCard #6 · 273 reactions
  7. Garden crew · 36 minLibrary opens an hour later on MondayCard #7 · 308 reactions
  8. Building team · 1 dWater is off for an hour on TuesdayCard #8 · 390 reactions

A fictional feed of 10,000 generated cards. This page always virtualises it in your browser, so the page stays smooth. The timeline models what a phone would do with the costs you switch on. The strip above the feed stands in for the header: it shows the top card and runs on the header’s thread, so a JS-driven header stops while JS is busy and the list keeps scrolling.

Frame timeline

Janky

Goal: Get the standard fling to zero dropped frames at 60 Hz, then at 120 Hz, changing as few switches as you can. The feed must still open in under a second.

Not smooth yet at 60 Hz.Switches changed from the first draft: 0

Gesture

Scroll the phone feed to record a gesture (up to 4 seconds).

Drops the viewer sees
90 / 90
JS frames missed
90
UI frames missed
32
Worst JS frame
93.2 ms559%
Worst UI frame
30.6 ms184%
On timeOver budgetSkipped, still busyDrop the viewer seesbudget 16.7 ms

What spent the budget

Frame 1 / 90 · 0.00 sdropped on screen

JS threadOver budget

23.2 of 16.7 ms · 139%

  • Scroll event and list window1.2 ms
  • Render new rows · 1 row1.4 ms
  • Row layout events · 1 row1.6 ms
  • Re-render the screen for header state0.8 ms
  • Re-render every mounted row · 121 rows18.2 ms

UI threadOver budget

16.70 of 16.67 ms · 100.2%

  • Draw and composite2.8 ms
  • Create native views · 1 row0.7 ms
  • Measure new rows · 1 row2.4 ms
  • Upload photos on first draw · 1 row10.8 ms

Over by only 0.03 ms, and still dropped: a frame that misses its deadline is not shown late, it is not shown at all. Android Developers: Slow rendering (opens in a new tab)

Opening the feed: 10 rows rendered before the first frame: 30 ms of JS, then 98.6 ms of UI work, 129 ms in all.

Costs

Janky90 of 90 frames dropped

Each switch adds one cost to the feed. Switch off as few as you can.

Model and limits

  • Every cost is an authored constant for a mid-range phone, chosen to show the shape of the problem. None of them is a measurement. A frame over budget by any amount counts as dropped, as on Android, so a few frames land just over the line.
  • Each lane starts work on a vsync. A lane that is still busy at the next vsync skips that frame, and its input waits, coalesced, for the next free frame.
  • The viewer sees a drop when the UI thread misses a frame, and the whole screen stops, or when the JS thread misses one while the header is animated from JS, and the header stops while the list keeps scrolling. Native scrolling does not wait for JS.
  • Rows render as they enter the FlatList window (21 screens, the default), and rows scrolled past the window are never rendered. The real list renders up to 10 new rows per batch (maxToRenderPerBatch), 50 ms apart (updateCellsBatchingPeriod). The model spreads that rate over frames, with at most one full batch at once. Scroll faster than that and the screen shows blank rows, as FlatList does.
  • Image cost is charged when a card is first drawn on screen with its content, because uploading a large bitmap to the GPU can take several milliseconds. A blank row costs nothing until it fills in. Decoding is not charged: the model assumes it happens on a background thread.
  • Not modelled: network time, garbage collection, the old bridge versus JSI, thermal throttling and memory limits.

Sources

Why this lab

Umut builds React Native and Expo apps, including the Pometop mobile app, Ritmiva and Bumble, his social travel app. This lab lays out the frame budget every screen in apps like these has to fit into. The numbers are not traces from those apps.