Shopify / Shopify/react-native-skia
[Android] Multiple <Image> elements at Canvas root bind to wrong SkImage (Skia 2.6.2 / Expo SDK 56)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 8.6k
- Forks
- 647
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 35
Description
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
| 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
useImagehook 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.tsx
import React, { useEffect } from "react";
import { StyleSheet, View } from "react-native";
import {
Canvas,
Image as SkiaImage,
useImage,
} from "@shopify/react-native-skia";
// Three DIFFERENT image assets. Pick three visually distinct PNGs.
const SRC_A = require("./assets/a.png"); // e.g. 512x512 realistic photo
const SRC_B = require("./assets/b.png"); // e.g. 256x256 cartoon
const SRC_C = require("./assets/c.png"); // e.g. 256x256 cartoon (different)
function Probe({ src, x, label }: { src: number; x: number; label: string }) {
const img = useImage(src);
useEffect(() => {
if (img) console.log(`[${label}]`, img.width(), img.height());
}, [img, label]);
if (img === null) return null;
return (
<SkiaImage image={img} x={x} y={10} width={120} height={120} fit="cover" />
);
}
export default function App() {
return (
<View style={styles.root}>
<Canvas style={styles.canvas}>
<Probe src={SRC_A} x={10} label="A" />
<Probe src={SRC_B} x={140} label="B" />
<Probe src={SRC_C} x={270} label="C" />
</Canvas>
</View>
);
}
const styles = 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
imageprop 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
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 #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.memoanduseMemoboundaries - 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)
Related issues found while debugging
- #3712 (picture intermittently not rendering)
- #3092 (useImage Android)
- #3643 (Canvas failing to paint)
- #2656 (text/image not rendering in lists)
…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.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the App.tsx minimal reproduction and verify the Android behavior with three distinct Skia Image children under one Canvas. Then trace the Android rendering and image-binding entry points for Canvas, Image, and useImage. Done means each sibling keeps its own pixels, prop swaps repaint, grouped images render, and re-renders do not shuffle bindings.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, react-native, typescript
- Domain
- mobile-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100