| name | jank-detection |
| description | Detect and attribute janky frames using Perfetto, Android Choreographer logs, Xcode Time Profiler, and Flutter DevTools. Use when users report stutters even though "everything looks fine" in dev. |
Jank Detection
Instructions
Jank is any frame that misses the vsync deadline. Single frames are invisible; runs of 2–5 missed frames are what users perceive as "the app stutters." Detecting jank requires capturing a trace under realistic conditions and reading it correctly.
1. Reproduce Under Realistic Conditions
- Release or profile build. Debug builds lie.
- Physical device, battery on (not plugged to a fast USB hub that can throttle).
- Thermal state: capture once cold, once after 2 min of the target workload.
- Airplane mode off but constrain bandwidth (Network Link Conditioner, Android
adb shell tc qdisc) to surface network-dependent jank.
2. Android — Perfetto Traces
Capture a trace while reproducing the scroll that janks:
adb shell perfetto -o /data/misc/perfetto-traces/scroll.pftrace -t 10s `
sched freq idle am wm gfx view binder_driver hal dalvik camera input res memory
adb pull /data/misc/perfetto-traces/scroll.pftrace
Open in ui.perfetto.dev. Key views:
- Frame Lifecycle track — rows per frame, colored green/yellow/red. Red = janky.
- Android FrameTimeline — select a red frame to jump to the slice chain that caused it.
- CPU usage per core — find the thread that was hot during the jank window.
- Custom trace sections — wrap suspect code with
androidx.tracing.Trace.beginSection("parseFeed").
3. Android — Choreographer Log
For a quick read without Perfetto:
adb logcat | Select-String "Choreographer"
You will see messages like Skipped 37 frames! The application may be doing too much work on its main thread. The stack trace that follows points at the offender.
Also useful:
adb shell dumpsys gfxinfo com.example.app
Look at the histogram of frame render times; p95 should match your budget.
4. iOS — Time Profiler + Core Animation
- Instruments → Time Profiler + Core Animation templates combined.
- Core Animation's FPS gauge drops below 60 (or 120 on ProMotion) on jank.
- In Time Profiler, set filter: Main Thread only, invert the call tree, look at hot frames during the drop.
- Add signposts to correlate your code with OS frames:
let signposter = OSSignposter(subsystem: "render", category: "feed")
let id = signposter.makeSignpostID()
let state = signposter.beginInterval("prepareCells", id: id)
defer { signposter.endInterval("prepareCells", state) }
Instruments shows signpost bars aligned with the frame track.
5. Flutter — DevTools Performance
- Run
flutter run --profile.
- DevTools → Performance tab → Frames chart. Red bars = janky.
- Click a red frame to see the UI-thread flamechart (Dart stack) and raster-thread flamechart (Skia/Impeller).
- If the UI flame is wide: reduce rebuilds, move work to isolates.
- If the raster flame is wide: reduce overdraw, consider
RepaintBoundary, check for shader compilation jank (first-run only).
Enable Enhance Tracing → Track widget builds to count rebuilds per frame.
6. React Native — Flipper + Systrace
-
Flipper → React DevTools Profiler: records component renders and their durations.
-
Systrace with JS markers:
import { Systrace } from 'react-native';
Systrace.beginEvent('parseFeed');
Systrace.endEvent();
-
Use the Performance Monitor overlay (Cmd+D → Perf Monitor). It shows UI and JS FPS; if JS drops to 20 FPS during interaction, the culprit is a blocking JS call (big JSON parse, sync storage).
-
On the New Architecture, use Fabric Tracing in the Flipper plugin to see shadow-tree commits per frame.
7. Common Culprits (attributed jank → fix)
| Symptom in trace | Likely cause | Fix |
|---|
Wide nativeRunLoopRun / Looper.loop gap | Main-thread IO | Move to background executor; cache parsed result. |
Wide Recomposer slice | Too many recompositions | Hoist state, use derivedStateOf, stable keys. |
Wide RenderProxy::syncFrameState | Large hardware layer upload | Reduce bitmap size; avoid huge AndroidView in Compose. |
iOS CATransaction commit > 8 ms | Layer tree is too deep / many sublayers animating simultaneously | Flatten layers; rasterize static subtree. |
| Flutter raster > UI | Overdraw, blurs, or shader compile | RepaintBoundary, precompile shaders, reduce blur radius. |
| RN JS FPS drops on list scroll | FlatList re-rendering whole rows or non-memoized renderItem | React.memo, getItemLayout, keyExtractor stable, or move to FlashList. |
8. Regression Gates
Use Macrobenchmark FrameTimingMetric on Android. On iOS, use XCTOSSignpostMetric combined with XCTClockMetric. Fail CI on p95 frame time regression > 10%.
Checklist