Android 17: TextureView presents only a fraction of rendered frames (updateTexImage drain from #1980 no longer works); SurfaceView opt-in proposal

Author: jslokCreated Sep 12, 2026Updated Sep 12, 2026

Description

On Android 17 the default SkiaTextureView path presents only a fraction of the frames Skia renders, so Reanimated-driven canvases look like a low-fps stepping animation. The same build is smooth on iOS and was smooth on the same device on Android 16.

Cause: the per-frame SurfaceTexture.updateTexImage() drain added in #1980 (the fix for #1960) no longer drains on Android 17. On Android ≤16 it fails after acquiring/releasing the pending buffer (updateAndRelease: EGLConsumer is not attached, #2054), so it still kept the TextureView in step by accident. On 17 it returns early:

E <app>: [SurfaceTexture-0-<pid>-5] updateTexImage: LegacySurfaceTexture is not attached!
I RNSkia  : updateAndRelease() failed. The exception above can safely be ignored

Evidence (Pixel 10, Android 17 build CP2A.260805.005, app render rate 60 Hz):

  • RNSkia log above fires every ~16.6 ms → Skia renders 60 fps.
  • dumpsys gfxinfo <pkg> framestats: HWUI presents the TextureView content in RenderThread-only frames (flag 2) at ~19–23/s, interleaved with main-thread frames every 2 vsyncs; ~44 HWUI frames/s total, intervals alternating 16.6/33.3 ms. GPU time 1 ms p99, so it is not GPU load.
  • Switching the same canvas to SkiaSurfaceView presents every frame and the animation is smooth (verified on the device).

React Native Skia Version

2.11.2

React Native Version

0.87.1 (react-native-reanimated 4.6.0, react-native-worklets 0.12.1)

Using New Architecture

  • Enabled

Steps to Reproduce

  1. Pixel 10 on Android 17.
  2. <Canvas> with a <Path> whose path is a useDerivedValue driven by withSpring shared values (any continuously animated Skia prop works; ours is a 4-corner polygon overlaying a camera preview).
  3. Retarget the springs continuously (or just let one settle) and watch the motion; enable Developer options → "Show refresh rate" to confirm the display is not the limit.
  4. adb logcat | grep -E "RNSkia|SurfaceTexture" and adb shell dumpsys gfxinfo <pkg> framestats while it animates.

Snack, Code Example, Screenshot, or Link to Repository

typescript
const x = useSharedValue(0)
useEffect(() => { x.value = withRepeat(withSpring(300), -1, true) }, [])
const path = useDerivedValue(() => {
  const p = Skia.Path.Make()
  p.addRect({ x: x.value, y: 100, width: 120, height: 160 })
  return p
})
<Canvas style={StyleSheet.absoluteFill}>
  <Path path={path} color="lime" style="stroke" strokeWidth={4} />
</Canvas>

Fix we are shipping (patched locally, tested working on a real Pixel 10): use SkiaSurfaceView, made usable as an overlay:

java
// SkiaSurfaceView constructor
getHolder().setFormat(PixelFormat.TRANSLUCENT); // EGL config already has alpha 8; renderer clears to transparent
setZOrderMediaOverlay(true);                    // above sibling SurfaceViews (camera preview), below the window's views

Today the only way to reach SkiaSurfaceView is opaque, and it is opaque black. Proposal: an opt-in androidSurfaceView prop on Canvas that selects the SurfaceView and applies the translucent format + media-overlay z-order when opaque is false, leaving opaque unchanged. Trade-offs to document: the canvas composites below the window's own views, and usual SurfaceView limitations during view transitions.

Should we submit a PR with this implementation?

Source: Shopify/react-native-skia