diff --git a/patches/@shopify/react-native-skia/@shopify+react-native-skia+2.4.18+004+defer-webgl-context-loss-to-unmount.patch b/patches/@shopify/react-native-skia/@shopify+react-native-skia+2.4.18+004+defer-webgl-context-loss-to-unmount.patch new file mode 100644 index 000000000000..d719f711d5af --- /dev/null +++ b/patches/@shopify/react-native-skia/@shopify+react-native-skia+2.4.18+004+defer-webgl-context-loss-to-unmount.patch @@ -0,0 +1,38 @@ +diff --git a/node_modules/@shopify/react-native-skia/lib/commonjs/views/SkiaPictureView.web.js b/node_modules/@shopify/react-native-skia/lib/commonjs/views/SkiaPictureView.web.js +index ec3b49e..a7ddd2b 100644 +--- a/node_modules/@shopify/react-native-skia/lib/commonjs/views/SkiaPictureView.web.js ++++ b/node_modules/@shopify/react-native-skia/lib/commonjs/views/SkiaPictureView.web.js +@@ -100,8 +100,12 @@ class WebGLRenderer { + } + dispose() { + if (this.surface) { +- var _this$canvas; +- (_this$canvas = this.canvas) === null || _this$canvas === void 0 || (_this$canvas = _this$canvas.getContext("webgl2")) === null || _this$canvas === void 0 || (_this$canvas = _this$canvas.getExtension("WEBGL_lose_context")) === null || _this$canvas === void 0 || _this$canvas.loseContext(); ++ // A lost context is permanent for the element and effect cleanup is not unmount (an ++ // hide or a StrictMode re-run keeps the ), so only lose it once React ++ // has detached the element (upstream Shopify/react-native-skia#4002). ++ if (!this.canvas.isConnected) { ++ this.canvas.getContext("webgl2")?.getExtension("WEBGL_lose_context")?.loseContext(); ++ } + this.surface.ref.delete(); + this.surface = null; + } +diff --git a/node_modules/@shopify/react-native-skia/lib/module/views/SkiaPictureView.web.js b/node_modules/@shopify/react-native-skia/lib/module/views/SkiaPictureView.web.js +index e1cd87b..4ea9833 100644 +--- a/node_modules/@shopify/react-native-skia/lib/module/views/SkiaPictureView.web.js ++++ b/node_modules/@shopify/react-native-skia/lib/module/views/SkiaPictureView.web.js +@@ -93,8 +93,12 @@ class WebGLRenderer { + } + dispose() { + if (this.surface) { +- var _this$canvas; +- (_this$canvas = this.canvas) === null || _this$canvas === void 0 || (_this$canvas = _this$canvas.getContext("webgl2")) === null || _this$canvas === void 0 || (_this$canvas = _this$canvas.getExtension("WEBGL_lose_context")) === null || _this$canvas === void 0 || _this$canvas.loseContext(); ++ // A lost context is permanent for the element and effect cleanup is not unmount (an ++ // hide or a StrictMode re-run keeps the ), so only lose it once React ++ // has detached the element (upstream Shopify/react-native-skia#4002). ++ if (!this.canvas.isConnected) { ++ this.canvas.getContext("webgl2")?.getExtension("WEBGL_lose_context")?.loseContext(); ++ } + this.surface.ref.delete(); + this.surface = null; + } diff --git a/patches/@shopify/react-native-skia/details.md b/patches/@shopify/react-native-skia/details.md index 5e02782df8f7..547aa8c5b2b2 100644 --- a/patches/@shopify/react-native-skia/details.md +++ b/patches/@shopify/react-native-skia/details.md @@ -97,3 +97,50 @@ - Upstream PR/issue: https://github.com/Shopify/react-native-skia/pull/3855 — the same fix, merged upstream on 2026-05-26. Drop this patch once the Skia dependency is bumped to >= 2.6.9. - E/App issue: https://github.com/Expensify/App/issues/98331, https://github.com/Expensify/App/issues/95905 - PR introducing patch: https://github.com/Expensify/App/pull/98437 + +### [@shopify+react-native-skia+2.4.18+004+defer-webgl-context-loss-to-unmount.patch](@shopify+react-native-skia+2.4.18+004+defer-webgl-context-loss-to-unmount.patch) + +- Reason: + + ``` + Fixes charts staying permanently blank on web whenever React re-runs the + layout effects of a mounted on the same DOM node: a screen wrapped + in React being hidden and revealed (the ScreenActivityWrapper + rollout) and StrictMode's DEV double-invoke. + + The effect cleanup calls WebGLRenderer.dispose(), which lost the WebGL + context of the . Per the WEBGL_lose_context spec that loss is + permanent for the element, so the renderer the re-run creates on the same + element can never get a surface again: CanvasKit.MakeWebGLCanvasSurface + throws "Cannot read properties of null (reading 'rangeMin')", which patch + 002 turns into the "unable to display chart" fallback. + + dispose() now only loses the context when the canvas has left the + document. In 2.4.18 it runs from a passive useEffect cleanup, which React + flushes after removing the host node, so isConnected is already false at + cleanup time on a real unmount, which keeps releasing the context + deterministically (browsers cap a page at ~16 live contexts). An Activity + hide keeps the element connected, so the context and the last presented + frame survive and the reveal builds its surface on the live context like + the resize path already does. The kept frame matters because + ScreenActivityWrapper keeps the hidden screen painted as a static backdrop + (AlwaysPaintedView). Upstream defers the same check by a microtask because + its dispose() runs from a layout effect cleanup, before the node is + detached; a passive caller needs no deferral. + + One case gets worse than before: a screen unmounted while hidden. Its + cleanup already ran at hide time, when the canvas was still connected, so + React runs none at unmount and the context, with its full-size drawing + buffer, is only reclaimed by the browser's LRU force-loss once the cap is + hit. Before the patch every unmount, background screen or not, released + its context synchronously. Mounted screens are unchanged, each held a live + context before Activity too. Upstream #4002 skips the same release, but it + is less exposed: its dispose() frees the surface, the GrContext, the + CanvasKit context handle and the drawing buffer at every hide, so only the + context slot survives there. Freeing the buffer is the canvas size reset + that would blank the backdrop, which is why this patch does not copy it. + ``` + +- Upstream PR/issue: https://github.com/Shopify/react-native-skia/issues/3976, fixed by https://github.com/Shopify/react-native-skia/pull/4002 (merged 2026-09-02, not in any release as of 2026-09-09; the latest is 2.11.2). Its dispose() applies the same isConnected guard (deferred by a microtask, since its caller is a layout effect) and adds context-restore handling, but it also zeroes the canvas size on cleanup, so a hidden Activity screen would show a blank chart in the backdrop. When the Skia dependency is bumped past that merge, either accept the blank backdrop and drop this patch or replace it with a patch that only removes the size reset. +- E/App issue: https://github.com/Expensify/App/issues/98254 +- PR introducing patch: https://github.com/Expensify/App/pull/100714