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.
Summary
On web,
SkiaPictureViewonly ever instantiates itsWebGLRenderer(orStaticWebGLRenderer) insideonLayoutEvent, which is wired to the wrapperPlatform.View'sonLayoutprop. ThatonLayoutis driven by react-native-web'sResizeObservershim and, in real-world layouts, does not always fire before the first picture is dispatched into the view. When that happens:renderer.currentstaysnulltickrAF loop runs forever, drainingredrawRequestsRefwithout ever callingrenderer.draw(...)<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(currentmain, also reproduces on the published@shopify/react-native-skia@2.2.12).Repro (minimal)
Run
npx expo start --web, open in Chrome — the<canvas>element exists in the DOM with the expected width/height but is fully transparent. Thetomatobackground and the dark overlay show through. Expected: a green rectangle.Root-cause confirmation
Add a temporary log in the published
tick():You'll see
renderer=nullfor the entire session while the drain count keeps climbing — pictures are being dispatched, but no renderer was ever constructed becauseonLayoutEventnever fired.Proposed fix
Initialize the renderer from a
useLayoutEffectkeyed on the<canvas>ref as soon as it has measurable layout, with a boundedrequestAnimationFrameretry loop in case the canvas hasn't been measured yet. KeeponLayoutEventas-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.jsto add theuseLayoutEffect. Happy to retire it once this lands.