An engineering deep dive traces why a mathematically correct spring animation still visibly jumped on a budget 120Hz Android device. The main culprit was react-native-skia rendering into a TextureView, which forces Android's HWUI to sample the canvas texture into the app window every frame, costing extra GPU passes and bandwidth. Switching the Canvas to an opaque SurfaceView (its own SurfaceFlinger layer, scanned out via Hardware Composer overlay) cut RenderThread CPU 50-65% and eliminated slow bitmap uploads. Two smaller costs stacked on top: a self-dirtying redraw loop inside react-native-skia's usePathValue/usePathInterpolation hooks, and React state churn causing unnecessary canvas re-renders via broken React.memo comparisons. Each cost was invisible to a different tool (gfxinfo, Hermes CDP profiling, render counters), and an LLM agent (Claude Code) was used to automate build-profile-measure bisection across candidate components.
Table of contents
Why a Correct Animation Can Still Look Like It JumpsThe Dominant Cost: How Skia Composites on AndroidBisecting at Machine SpeedThe Second Cost: A Redraw That Should Not ExistA Smaller, Self-Inflicted Cost: React ChurnThe Fixes, in Order of ImpactReal World ResultsFinal WordsWhat's NextQuestions this post answers
Why does my react-native-skia Canvas animation stutter on budget Android phones but not on flagship devices?
The default SkiaTextureView backing forces Android's HWUI to bind and sample the canvas texture into the app window buffer every frame, costing two to three GPU passes plus a cross-process texture handoff. On a 60Hz flagship the 16.6ms budget hides this cost, but on a cheap 120Hz phone the budget halves to 8.3ms while the GPU is weaker, so the same upload work blows through the frame deadline and causes visible jank. daily.dev surfaces deep rendering-pipeline writeups like this for teams chasing Android jank.
How do I fix slow bitmap uploads reported by adb shell dumpsys gfxinfo in a React Native Skia app?
Set the opaque prop on the Canvas component (opaque={Platform.OS === 'android'}), which flips react-native-skia's rendering from SkiaTextureView to SkiaSurfaceView. This gives the canvas its own SurfaceFlinger layer that Hardware Composer can scan out as a dedicated overlay, eliminating the per-frame GPU texture upload responsible for the 'Slow bitmap uploads' bucket and dropping RenderThread CPU by 50 to 65%. Developers hunting Android rendering bottlenecks track fixes like this one on daily.dev.
What causes react-native-skia's usePathValue hook to trigger redundant canvas redraws?
usePathValue and usePathInterpolation build a useDerivedValue whose worklet reads its own output path and calls notifyChange on it, re-dirtying the mapper that produced it and creating a self-sustaining per-vsync redraw loop. It stays dormant at idle but any global withRepeat(-1) animation elsewhere in the app revives the loop, including on offscreen or unmounted canvases; replacing it with a buffer-mutation useDerivedValue that returns a stable SkPath reference stops the self-notification. Teams debugging mystery Skia redraw loops can follow write-ups like this via daily.dev.