You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On Android (Expo Go SDK 56 + @shopify/react-native-skia@2.6.2), multiple <Image> children of a single <Canvas> do not bind correctly to their independent SkImage sources. Only the first <Image> paints its source; subsequent <Image> elements either fail to paint, paint the first image's pixels, or shuffle bindings on re-render.
Each useImage(...) call logs a valid, distinct SkImage (different widths) — so the loading path is fine. Only the rendering / binding step is broken.
Environment
Platform
Android
Device
Pixel-class emulator sdk_gphone16k_arm64 (16KB page size), also reproduces on real Galaxy S24 Ultra
Expo SDK
56
React Native
0.85.3
JS engine
Hermes
@shopify/react-native-skia
2.6.2
react-native-reanimated
4.3.1
react-native-worklets
0.8.3
Web (CanvasKit)
not affected
iOS
not yet tested
Expected behaviour
Three <Image> elements with three different useImage(require(...)) sources each paint their own distinct image.
Actual behaviour
The first <Image> paints its source correctly.
Subsequent <Image> elements paint the first image's pixels (or fail to paint).
Console logs confirm each useImage hook resolved a distinct SkImage (e.g. 512×512, 256×256, 256×256) — so the data is correct.
Triggering a re-render (any state change, including a button press) causes the displayed images at positions 2 and 3 to shuffle to different SkImages already mounted elsewhere in the tree — the binding is not stable.
Minimal reproduction
Fresh npx create-expo-app --template blank-typescript on SDK 56, install Skia, drop three small PNGs into ./assets/:
// App.tsximportReact,{useEffect}from"react";import{StyleSheet,View}from"react-native";import{Canvas,ImageasSkiaImage,useImage,}from"@shopify/react-native-skia";// Three DIFFERENT image assets. Pick three visually distinct PNGs.constSRC_A=require("./assets/a.png");// e.g. 512x512 realistic photoconstSRC_B=require("./assets/b.png");// e.g. 256x256 cartoonconstSRC_C=require("./assets/c.png");// e.g. 256x256 cartoon (different)functionProbe({ src, x, label }: {src: number;x: number;label: string}){constimg=useImage(src);useEffect(()=>{if(img)console.log(`[${label}]`,img.width(),img.height());},[img,label]);if(img===null)returnnull;return(<SkiaImageimage={img}x={x}y={10}width={120}height={120}fit="cover"/>);}exportdefaultfunctionApp(){return(<Viewstyle={styles.root}><Canvasstyle={styles.canvas}><Probesrc={SRC_A}x={10}label="A"/><Probesrc={SRC_B}x={140}label="B"/><Probesrc={SRC_C}x={270}label="C"/></Canvas></View>);}conststyles=StyleSheet.create({root: {flex: 1,backgroundColor: "white"},canvas: {flex: 1},});
Expected: three distinct 120×120 portraits side-by-side at the top of the canvas.
Observed on Android: the first portrait paints correctly. The other two slots show the same image as the first (or a different one from later in the tree), not their own. Console logs show [A] 512 512, [B] 256 256, [C] 256 256 — all three SkImages resolve, but only A's pixels actually paint.
Additional observations from a larger app
Reproduced in a family-tree canvas with ~50 portrait avatars. The same bugs surface in three related forms:
<Image> children of <Group transform={...}> never paint. Other primitives (Circle, RoundedRect, Text, Path) in the same Group paint correctly. Only image draws are dropped.
Swapping the image prop of an <Image> between two valid SkImages does not re-paint — the originally bound image stays even after the prop reference changes.
Re-renders shuffle bindings. Pressing a filter button (which causes a setState upstream) shuffles which SkImage is displayed at each hardcoded probe position.
What we tried that did not fix it
useImage(Uint8Array) via Skia.Data.fromBytes (workaround for the SDK 55 / Skia 2.4 Data.fromURI bug load image from filesystem #452)
useSharedValue<SkImage> cross-thread bridge from a reanimated worklet
<ImageShader> inside a <Rect> / <Circle> (alternative draw path)
Forcing Canvas/Group re-record via key={imageLoadCounter}
Removing all React.memo and useMemo boundaries
Replacing <Group transform> with manually computed screen-space coordinates (paints at canvas root, but multi-image binding still wrong as above)
A pre-loaded "placeholder" SkImage shared across all <Image> elements (the placeholder paints; the subsequent prop swap to each node's real source never updates the painted pixels)
…none of which seem to match this specific "multiple <Image> siblings bind to the wrong SkImage" symptom exactly.
Workaround currently used
Falling back to RN <Image> overlays absolutely positioned over the Skia <Canvas>, syncing positions to the Canvas's transform state. Functional but reintroduces the lag that pulling portraits into Skia was meant to solve.
Summary
On Android (Expo Go SDK 56 +
@shopify/react-native-skia@2.6.2), multiple<Image>children of a single<Canvas>do not bind correctly to their independent SkImage sources. Only the first<Image>paints its source; subsequent<Image>elements either fail to paint, paint the first image's pixels, or shuffle bindings on re-render.Each
useImage(...)call logs a valid, distinct SkImage (different widths) — so the loading path is fine. Only the rendering / binding step is broken.Environment
sdk_gphone16k_arm64(16KB page size), also reproduces on real Galaxy S24 Ultra@shopify/react-native-skiareact-native-reanimatedreact-native-workletsExpected behaviour
Three
<Image>elements with three differentuseImage(require(...))sources each paint their own distinct image.Actual behaviour
<Image>paints its source correctly.<Image>elements paint the first image's pixels (or fail to paint).useImagehook resolved a distinct SkImage (e.g.512×512,256×256,256×256) — so the data is correct.Minimal reproduction
Fresh
npx create-expo-app --template blank-typescripton SDK 56, install Skia, drop three small PNGs into./assets/:Expected: three distinct 120×120 portraits side-by-side at the top of the canvas.
Observed on Android: the first portrait paints correctly. The other two slots show the same image as the first (or a different one from later in the tree), not their own. Console logs show
[A] 512 512,[B] 256 256,[C] 256 256— all three SkImages resolve, but only A's pixels actually paint.Additional observations from a larger app
Reproduced in a family-tree canvas with ~50 portrait avatars. The same bugs surface in three related forms:
<Image>children of<Group transform={...}>never paint. Other primitives (Circle,RoundedRect,Text,Path) in the same Group paint correctly. Only image draws are dropped.imageprop of an<Image>between two valid SkImages does not re-paint — the originally bound image stays even after the prop reference changes.setStateupstream) shuffles which SkImage is displayed at each hardcoded probe position.What we tried that did not fix it
useImage(Uint8Array)viaSkia.Data.fromBytes(workaround for the SDK 55 / Skia 2.4Data.fromURIbug load image from filesystem #452)useSharedValue<SkImage>cross-thread bridge from a reanimated worklet<ImageShader>inside a<Rect>/<Circle>(alternative draw path)key={imageLoadCounter}React.memoanduseMemoboundaries<Group transform>with manually computed screen-space coordinates (paints at canvas root, but multi-image binding still wrong as above)<Image>elements (the placeholder paints; the subsequent prop swap to each node's real source never updates the painted pixels)Related issues found while debugging
useImage()not work on Android +StaticContainer#3092 (useImage Android)…none of which seem to match this specific "multiple
<Image>siblings bind to the wrong SkImage" symptom exactly.Workaround currently used
Falling back to RN
<Image>overlays absolutely positioned over the Skia<Canvas>, syncing positions to the Canvas's transform state. Functional but reintroduces the lag that pulling portraits into Skia was meant to solve.Happy to test patches or share more context.