Skip to content

Web: SkiaPictureView canvas stays blank when wrapper onLayout doesn't fire before the first picture #3829

@svavarstudio

Description

@svavarstudio

Summary

On web, SkiaPictureView only ever instantiates its WebGLRenderer (or StaticWebGLRenderer) inside onLayoutEvent, which is wired to the wrapper Platform.View's onLayout prop. That onLayout is driven by react-native-web's ResizeObserver shim and, in real-world layouts, does not always fire before the first picture is dispatched into the view. When that happens:

  • renderer.current stays null
  • The tick rAF loop runs forever, draining redrawRequestsRef without ever calling renderer.draw(...)
  • The <canvas> is never painted — it stays fully transparent and the user sees whatever is behind it (e.g. the parent View's CSS gradient), which makes it look like "only part of the scene rendered"

This is not a logic bug in the consumer's render code — pictures are being produced and dispatched correctly, they just never reach a renderer.

Affected file

packages/skia/src/views/SkiaPictureView.web.tsx (current main, also reproduces on the published @shopify/react-native-skia@2.2.12).

Repro (minimal)

// App.tsx — Expo + react-native-web
import { Canvas, Rect } from "@shopify/react-native-skia";
import { View } from "react-native";

export default function App() {
  return (
    <View style={{ flex: 1, backgroundColor: "tomato" }}>
      {/* Sibling overlay on top of the canvas — this is the kind of layout
          where the wrapper Platform.View's onLayout (ResizeObserver) doesn't
          fire before the first picture is dispatched. */}
      <View
        style={{ position: "absolute", top: 0, left: 0, right: 0, height: 60,
                 backgroundColor: "rgba(0,0,0,0.4)", zIndex: 10 }}
      />
      <Canvas style={{ width: 360, height: 520, alignSelf: "center" }}>
        <Rect x={0} y={0} width={360} height={520} color="lime" />
      </Canvas>
    </View>
  );
}
// index.js — load CanvasKit before the App
import { LoadSkiaWeb } from "@shopify/react-native-skia/lib/module/web";
import { registerRootComponent } from "expo";

LoadSkiaWeb({
  locateFile: (file) =>
    `https://cdn.jsdelivr.net/npm/canvaskit-wasm@0.40.0/bin/full/${file}`,
}).then(async () => {
  const App = (await import("./App")).default;
  registerRootComponent(App);
});

Run npx expo start --web, open in Chrome — the <canvas> element exists in the DOM with the expected width/height but is fully transparent. The tomato background and the dark overlay show through. Expected: a green rectangle.

Root-cause confirmation

Add a temporary log in the published tick():

console.log("renderer=", renderer.current, "drains=", redrawRequestsRef.current);

You'll see renderer=null for the entire session while the drain count keeps climbing — pictures are being dispatched, but no renderer was ever constructed because onLayoutEvent never fired.

Proposed fix

Initialize the renderer from a useLayoutEffect keyed on the <canvas> ref as soon as it has measurable layout, with a bounded requestAnimationFrame retry loop in case the canvas hasn't been measured yet. Keep onLayoutEvent as-is for the resize path, but guard it so it doesn't clobber an already-constructed renderer.

PR: #3830

Workaround (downstream)

We currently maintain a post-install patch that mutates the compiled lib/module/views/SkiaPictureView.web.js to add the useLayoutEffect. Happy to retire it once this lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions