Skip Skia web charts when WebGL shader precision is unavailable#92874
Conversation
CanvasKit reads .rangeMin off getShaderPrecisionFormat() without a null check, so on devices that cannot create a usable WebGL context it throws an uncaught TypeError during async GL init. Probe WebGL capability up front in a shared chart wrapper and render the existing fallback view instead of mounting Skia when the environment can't support it.
…eb-webgl-precheck
- Keep the `object` generic constraint (mirrors WithSkiaWeb) with a scoped no-restricted-types disable: Record<string, unknown> rejects the interface-based VictoryChartRendererProps that lacks an index signature. - Drop the now-unnecessary WebGL context type assertion (TS infers it). - Trim the chart wrapper doc comments to keep the load-bearing "why".
Cover the WebGL capability probe (supported, no-context, null precision format, throw, memoization) and the wrapper's branch that renders the fallback instead of mounting Skia when WebGL is unsupported.
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
The session-wide memo cached the first probe result, so a WebGL context lost after the first chart rendered (or any later capability change) would still read the stale "supported" value, mount Skia and hit the same crash. Probe once per chart mount via a lazy initializer (cheap, and re-rendering no longer recreates contexts) and release the throwaway probe context so it can't evict a real chart's context.
|
@mountiny @ShridharGoel @Expensify/design On WebGL-incapable devices, what should the chart area show instead of the infinite loading spinner?
current behavior Kapture.2026-06-08.at.07.32.23.mp4 |
|
@wildan-m What devices are we talking about here, I assume we should have some fallback for these @mateuuszzzzz |
|
Can we fallback on SVG-powered charts? |
On devices that can't give Skia a usable WebGL surface, render a localized message telling the user to enable WebGL in their browser settings, instead of the loading spinner — which never resolved (looked like infinite loading) and fired long-loading/skeleton telemetry. New en/es strings; copy pending JaimeGPT + #expensify-open-source approval.
|
Hey, I noticed you changed If you want to automatically generate translations for other locales, an Expensify employee will have to:
Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running: npx ts-node ./scripts/generateTranslations.ts --helpTypically, you'd want to translate only what you changed by running |
|
@roryabraham Open to that. Only catch: the charts run on victory-native v41, which renders only via Skia/WebGL (no SVG mode), so an SVG fallback would be a separate implementation — bar/line/pie plus the Victory HTML renderer (arbitrary chart specs from messages). |
|
I think adding the fallback will be extra work and should be handled in a follow up given right now the app crashes when we run into this issue So I suggest we add some @Expensify/design graphic to explain that this browser does not support the charts and we look at the scale of this issue and whether its worth adding the fallback How should the "unavailable state" look like @Expensify/design ? |
What kind of devices are these typically? How often is this happening? We have plenty of empty state patterns we can use as a placeholder, but I don't know what the correct messaging should be. Do we tell the user to try a different browser? Something else? |
|
From sentry its 70% windows and its chromium only so Edge and Chrome browsers. it happend to 74 unique users in the past 90 days, so its not a large amount, but it does not look like there would be a clear common denominator. It also happened on the latest Chrome versions on Mac and Windows, so it also depends on whether the device they are using supports it, I guess. |
|
Do you have any suggestions for what copy we would write in the placeholder state? |
|
Your client does not support the required WebGL technology for this feature. Make sure to enable it or use a different browser. Maybe something like this? I think its worth it to mention the exact technology so they could potentially google this and enable or choose different browser |
sounds good |
|
That feels right and simple to me 👍 |
|
same |
|
Agree - no need to reinvent the wheel here. I think that works well. |
|
@shawnborton The "monitor with a red X" illustration from the mockup isn't in the codebase yet — which should we use? Closest are the |
Replace the single-line message with the approved empty state: placeholder Monitor icon + "Unable to display chart" + "Your browser doesn't support WebGL. Please enable it or switch browsers." (en + es; es via JaimeGPT). The other languages come from generateTranslations, which skips fork PRs. Also rewrite the chart tests cast-free so they pass the type-aware lint (the previous mock casts failed CI ESLint).
|
Here is the illustration you need: simple-illustration__monitorsync-no.svg.zip |
Add the design-provided simple-illustration__monitorsync-no.svg, register it as MonitorSyncNo, and render it in the chart empty state in place of the placeholder Monitor icon.
…eb-webgl-precheck
|
@mountiny Can we run the translations workflow here? |
Reviewer Checklist
Screenshots/VideosMacOS: Chrome / SafariScreen.Recording.2026-07-01.at.10.05.20.AM.mov |
🦜 Polyglot Parrot! 🦜Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues: View the translation diffdiff --git a/src/languages/es.ts b/src/languages/es.ts
index 8a6d1bd47ac..b1281eee150 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -22,7 +22,7 @@ const translations: TranslationDeepObject<typeof en> = {
count: 'Contar',
cancel: 'Cancelar',
unableToDisplayChart: 'No se puede mostrar el gráfico',
- webGLNotSupported: 'Tu navegador no es compatible con WebGL. Por favor, actívalo o cambia de navegador.',
+ webGLNotSupported: 'Tu navegador no es compatible con WebGL. Por favor, habilítalo o cambia de navegador.',
dismiss: 'Descartar',
proceed: 'Proceder',
unshare: 'Dejar de compartir',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 262fa875c43..55abfebfaf2 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -515,7 +515,7 @@ const translations: TranslationDeepObject<typeof en> = {
restrictions: '制限',
off: 'オフ',
unableToDisplayChart: 'グラフを表示できません',
- webGLNotSupported: 'お使いのブラウザは WebGL に対応していません。WebGL を有効にするか、別のブラウザに切り替えてください。',
+ webGLNotSupported: 'お使いのブラウザは WebGL に対応していません。有効にするか、別のブラウザに切り替えてください。',
},
socials: {
podcast: 'ポッドキャストでフォロー',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 7c743cea377..e479d752f2c 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -515,7 +515,7 @@ const translations: TranslationDeepObject<typeof en> = {
restrictions: 'Ograniczenia',
off: 'Wyłączone',
unableToDisplayChart: 'Nie można wyświetlić wykresu',
- webGLNotSupported: 'Twoja przeglądarka nie obsługuje WebGL. Włącz je lub zmień przeglądarkę.',
+ webGLNotSupported: 'Twoja przeglądarka nie obsługuje WebGL. Włącz ją albo zmień przeglądarkę.',
},
socials: {
podcast: 'Śledź nas na Podcast',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 2acd25e9909..aa90e3beedd 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -514,7 +514,7 @@ const translations: TranslationDeepObject<typeof en> = {
restrictions: 'Restrições',
off: 'Desligado',
unableToDisplayChart: 'Não foi possível exibir o gráfico',
- webGLNotSupported: 'Seu navegador não oferece suporte a WebGL. Ative o recurso ou use outro navegador.',
+ webGLNotSupported: 'Seu navegador não é compatível com WebGL. Ative-o ou mude de navegador.',
},
socials: {
podcast: 'Siga-nos no Podcast',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 7099c7d3b8d..ba450e6632d 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -511,7 +511,7 @@ const translations: TranslationDeepObject<typeof en> = {
restrictions: '限制',
off: '关',
unableToDisplayChart: '无法显示图表',
- webGLNotSupported: '您的浏览器不支持 WebGL。请启用它或更换浏览器。',
+ webGLNotSupported: '您的浏览器不支持 WebGL。请启用该功能或更换浏览器。',
},
socials: {
podcast: '在播客上关注我们',
Note You can apply these changes to your branch by copying the patch to your clipboard, then running |
…eb-webgl-precheck
Applies the OpenAI-generated translation improvements from the generateTranslations (Polyglot Parrot) run for es, ja, pl, pt-BR, and zh-hans.
|
Thanks @ShridharGoel. Addressed both comment nits in 7f016d6. |
|
@wildan-m conflict |
…eb-webgl-precheck # Conflicts: # src/languages/de.ts # src/languages/fr.ts # src/languages/it.ts # src/languages/ja.ts # src/languages/nl.ts # src/languages/pl.ts # src/languages/pt-BR.ts # src/languages/zh-hans.ts
|
This is still pending, can you check? #92874 (comment) I don't think the comment is needed, let me know if you think otherwise |
Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com>
|
🚧 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.27-1 🚀
|
|
🤖 No help site changes required — no draft docs PR created. Why: This PR is a defensive crash fix, not a product/behavior change. On GPUs that can't supply a WebGL shader-precision format, Skia's web backend was throwing Nothing here changes a documented workflow, feature, setting, tab, or button:
The Search insights charts (Spend over time, Top categories, Top merchants) are not currently documented in If you believe a specific help article should call out chart-rendering requirements or the unsupported-device fallback, let me know which article and I'll draft the update. |
|
🚀 Deployed to production by https://github.com/grgia in version: 9.4.27-2 🚀
Bundle Size Analysis (Sentry): |

Explanation of Change
On web, some devices crash the whole page with
TypeError: Cannot read properties of null (reading 'rangeMin')(Sentry APP-8SV, 54 users). It's reported on bank-account onboarding, but the cause is charts, not that form. When a chart mounts, Skia's web backend (CanvasKit) sets up WebGL by callinggetShaderPrecisionFormat()and reading.rangeMinfrom the result without checking for null. On devices that can't supply a precision format (software or blocked GPUs), that call returns null, so CanvasKit throws — and because it happens during async setup outside React, no error boundary catches it and the page goes down.This adds a web-only check that runs the same
getShaderPrecisionFormat()test before loading Skia. If it fails, the chart shows an "Unable to display chart" empty state instead of mounting Skia and crashing. The check lives in one shared wrapper used by all web charts (pie, line, bar, and the Victory HTML renderer) and runs on each chart mount. Capable devices are unaffected, and native charts are untouched (this path is web-only).Fixed Issues
$ #92418
PROPOSAL: #92418 (comment)
Tests
The Sentry crash only reproduces on devices where WebGL cannot supply a shader-precision format, so the unsupported path is simulated by forcing that condition.
TypeError: Cannot read properties of null (reading 'rangeMin')appears in the console. (Before this change, the same step throws that error.)Platform scope: Steps 3–5 simulate the incapable GPU through the browser console, so they apply to MacOS: Chrome/Safari (desktop). On the other platforms the override can't be injected on-device, so just confirm there's no regression — the Spend over time, Top categories, and Top merchants charts still render normally:
mWeb (Android Chrome / iOS Safari): the fix is active here too (mWeb uses the same web chart wrapper), but the GPU override can only be injected via remote debugging (chrome://inspect / Safari Web Inspector) — not required for routine testing.
iOS / Android Native: not affected — native charts render their
*ChartContentdirectly and never touchSkiaWebChart/CanvasKit.Verify that no errors appear in the JS console
Local web verification evidence (MacOS Chrome, dev server)
getShaderPrecisionFormat()to returnnull(the exact Sentry crash condition) and opening a chart, the chart area shows the "Unable to display chart" empty state,document.querySelectorAll('canvas').length === 0(Skia/CanvasKit never mounts), and norangeMinTypeErroror other console error is thrown — the page does not crash.Offline tests
This change does not affect offline behavior. The WebGL capability probe is local and network-independent, so chart rendering on capable devices and the fallback on incapable ones behave identically online and offline.
QA Steps
Regression only — no GPU simulation (the unsupported-device path is covered in the Tests section on desktop web). On each platform:
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, 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.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Kapture.2026-06-30.at.23.27.40.mp4
Android: mWeb Chrome
Kapture.2026-07-01.at.05.53.25.mp4
iOS: Native
Kapture.2026-06-30.at.23.17.33.mp4
iOS: mWeb Safari
Kapture.2026-06-30.at.23.20.45.mp4
MacOS: Chrome / Safari
Kapture.2026-06-30.at.22.57.56.mp4