All notes
08Note4 min

Two canvases, one decision

  • React
  • WebGL
  • Debugging

A context provider whose scope was one element too small, and the silent fallback bug it caused on machines where WebGL worked fine.

This site draws the same object twice: full-bleed behind the page, and again as an interactive diagram inside the philosophy section. Both need to answer one question — can this machine run WebGL? — and they must never answer it differently. An SVG onion behind a WebGL diagram would look broken in a way that is hard to even describe to a client.

So the decision is made once, by a provider, and consumed by both. That part was right from the start. The scope was not.

tsx
// ✗ the bug: provider closes before its second consumer
<RendererProvider>
  <CanvasMount />
  <FallbackObject />
</RendererProvider>
<main>
  <Services />   {/* useActiveRenderer() here reads the context DEFAULT */}
</main>

The philosophy diagram sat outside the provider. useActiveRenderer returned the context default — 'pending' — and never changed, because nothing was ever going to update a value it was not subscribed to.

Why nobody noticed for weeks

The 'pending' branch deliberately renders the SVG rather than nothing. That is the correct behaviour for the real pending case: the probe resolves on hydration, and showing an empty square for that frame reads as a broken layout.

The fallback was good enough to hide the bug. Every machine rendered the flat diagram, including the ones with working WebGL behind it, and it looked entirely intentional.

This is the failure mode of a well-built fallback and it is worth naming: a graceful degradation path that is indistinguishable from success will conceal the fault that triggers it. The fix was moving one closing tag. Finding it took considerably longer.

What we changed beyond the fix

  • The provider now wraps everything below it, with a comment at the closing tag explaining that the scope is load-bearing and why.
  • The diagram's centre readout prints 'Click a layer' under WebGL and 'Select a layer' under the fallback — a one-word difference that makes the active path visible on screen.
  • Context lost on the second canvas is now handled explicitly. Browsers drop the least-recently-used context under GPU pressure, and this is the one that goes.

The third point is the one most likely to bite someone else. Two WebGL contexts on a page is two more than the browser guarantees you, and the second one is not a hypothetical casualty.

We build this way for clients too.

Start a project