Skip to content

[Android] Multiple <Image> elements at Canvas root bind to wrong SkImage (Skia 2.6.2 / Expo SDK 56) #3873

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 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.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:

  1. <Image> children of <Group transform={...}> never paint. Other primitives (Circle, RoundedRect, Text, Path) in the same Group paint correctly. Only image draws are dropped.
  2. 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.
  3. 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)

Related issues found while debugging

…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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions