Render Skia web charts on a software surface when WebGL is unavailable - #97219
Conversation
CanvasKit throws from MakeWebGLCanvasSurface when it cannot create a WebGL2 context. That call runs in a ResizeObserver callback, so the throw escapes as an unhandled error no try/catch or error boundary can reach. Catch it and fall back to CanvasKit's software surface, so the chart still renders instead of crashing the page. This mirrors the library's other web renderer path, which already treats a failed WebGL surface as recoverable.
|
|
Place the patch alongside the existing react-native-skia patch as 002, following the naming convention, and document it in details.md as the patch guide requires.
…ebgl-surface-fallback
MakeSWCanvasSurface stores the canvas and calls getContext("2d") later, at flush
time, so it succeeds even for a canvas that can never provide one. A canvas that
already holds a WebGL context - a chart that rendered before WebGL became
unavailable - returns null there, turning the fallback into an uncatchable
"Cannot read properties of null (reading 'putImageData')". Check the 2D context
before creating the surface, leaving such a chart blank but stable.
When neither a WebGL nor a software surface can be created, the renderer previously went blank in silence, leaving the host application no way to react. Dispatch a bubbling "skiasurfaceunavailable" event on the canvas at that point - a no-op without a listener - so the chart wrapper can show its empty state instead of a blank canvas.
The capability probe runs when the chart mounts, but the drawing surface is created only after the CanvasKit WASM module loads, so the probe can pass while surface creation later fails - leaving a blank chart with no crash. Listen for the renderer's surface-unavailable event on a wrapper around the chart and swap in the existing "unable to display chart" empty state when it fires.
|
@wildan-m Could you merge main? |
Reviewer Checklist
Screenshots/VideosMacOS: Chrome / SafariScreen.Recording.2026-08-03.at.14.34.57.mov |
|
@codex review |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
|
@truph01 any feedback? |
| display chart" empty state instead of leaving a blank canvas. | ||
| ``` | ||
|
|
||
| - Upstream PR/issue: 🛑 TODO — the same defensive handling should be sent upstream so `onResize` matches the already-guarded `renderPictureToSurface` path. |
There was a problem hiding this comment.
Done — upstream PR: Shopify/react-native-skia#3996. It applies the same defensive handling (plus the skia-surface-unavailable event) to upstream main, where the throws moved into the renderer constructor and onResize after the WebGL-context-reuse refactor. details.md now links it; once it lands in a release we consume, the patch can be dropped.
The defensive handling is now proposed upstream, so the patch note points to that PR instead of a TODO.
|
@mountiny Updated — the upstream PR is filed (Shopify/react-native-skia#3996) and linked from |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.52-0 🚀
|
No help site changes requiredI reviewed this PR against the help articles under Why: This is a purely technical rendering-resilience fix for the web Skia charts. When a client can't create a WebGL surface (a rare, environmental GPU condition), the chart now falls back to CanvasKit's software surface, or — if no surface can be created at all — reuses the existing "Unable to display chart" empty state. It introduces no new user-facing feature, setting, tab, label, or workflow, and it doesn't change the documented behavior of Insights. The Insights help articles (e.g. How to Use Insights in Expensify, Top Merchants / Top Categories / Spend over time) describe how to access and use the reports — access rules, report types, view modes, and exporting. None of them document chart rendering internals or the GPU-failure fallback, so there's nothing to update. The "Unable to display chart" state already existed before this change and isn't described in the help site. If you believe a user-facing behavior did change in a way the docs should capture, reply with the specifics and I'll draft the update. |
Explanation of Change
failed to create webgl context: err 0(Sentry APP-7MV) is still reported on production after the prior fix, now on/searchas well as/home. That fix hardened the capability check that gates the web charts, and it does prevent the crash when the device is already incapable at the moment the chart mounts. But the check runs when the chart mounts, while the WebGL context is created much later — after the CanvasKit WASM module finishes loading. Anything that changes GPU availability in between (memory pressure, a GPU process restart, or the per-page context limit being exhausted) makes that verdict stale, and the check cannot see it: it validates a context it already holds, rather than the browser's ability to hand out a new one.So the crash cannot be fully prevented by predicting it up front. This makes the failure non-fatal where it actually happens instead: CanvasKit throws rather than degrading when it can't create a WebGL surface, and that throw runs in a resize callback, so no
try/catchor error boundary can reach it. The patch catches it and falls back to CanvasKit's software surface, so the chart still renders — mirroring the library's other web renderer path, which already treats a failed WebGL surface as recoverable. The software fallback is skipped for a canvas that already holds a WebGL context (it can never provide the 2D context the software surface flushes through, which would turn into another uncatchable error).When neither surface can be created, the renderer announces it with a bubbling DOM event — a no-op without a listener — and the chart wrapper listens for it and swaps in the existing "Unable to display chart" empty state. The capability check cannot cover that window, so the renderer's own report is the only reliable signal. Charts on capable clients are unchanged and still render on the GPU.
Fixed Issues
$ #97104
Tests
Go to Search → Spend → Insights and open Spend over time, Top categories, and Top merchants. Verify each chart renders normally (GPU path, unchanged).
Reload, and before opening a chart, simulate a client that can create a context for the capability check but not for the actual chart — paste this in the console:
Do not reload — navigate to an insight chart from the left sidebar so the chart mounts with the override active.
Verify the page does not crash — no
failed to create webgl context: err 0(or any other uncaught error) in the console — and the chart area shows one of the two degraded outcomes instead of staying silently blank:Before this change the error was thrown and the chart area stayed blank. (The error is thrown as a plain string rather than an
Error, so it has no stack trace — which is why it reaches Sentry as a bare title with no first-party frames.)Reload to clear the override and reopen a chart; verify it renders normally again on the GPU path.
Platform scope: step 2 injects through the browser console, so it applies to MacOS: Chrome/Safari. On other platforms just confirm no regression — the three insight charts still render normally. iOS/Android native are unaffected: the patched file is the web renderer, which native never loads.
Offline tests
No offline impact — the surface creation and fallback behave identically online and offline.
QA Steps
The crash condition is environmental and can't be reliably forced on staging, so just confirm there's no regression around the charts:
The definitive result is only observable in production: once this deploys, the
APP-7MVSentry issue should stop receiving newfailed to create webgl contextevents.PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)Avatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Kapture.2026-07-29.at.13.06.50.mp4