The 16-Context Limit: How shaderjoy Multiplexes WebGL

By rendering to a single hidden framebuffer and transferring pixels to standard 2D canvases, Chenglou's zero-dependency playground bypasses a hard browser constraint.

6 min read • View on GitHub • More from chenglou

A massive industrial projector casts a dense beam of light into a glass prism, which splits the beam into dozens of individual picture frames on a gallery wall. This represents the single WebGL context multiplexing output to many 2D canvases.
The WebGL context limit forces developers to rethink rendering architecture when scaling up.

Shaders art made with pure CSS, with an editable highlighted code area also made in pure CSS. Zero JavaScript!

Cheng Lou, Author · Pure CSS Shaders Art
Key Takeaways

The Context Ceiling

Modern web development is obsessed with frameworks, but frameworks cannot save you from hard browser constraints. If you have ever tried to build a grid of 20 standard WebGL canvases, you have likely encountered the silent killer: WEBGL_lose_context. Browsers are actively hostile toward multiple WebGL contexts to prevent GPU memory exhaustion. They enforce a strict ceiling, typically capping at 8 to 16 active contexts.

When that limit is breached, older contexts are forcefully discarded, or the page simply crashes. For a shader playground designed to display dozens of live previews simultaneously, this constraint presents a fundamental architectural roadblock.

The Multiplexing Illusion

The most fascinating aspect of shaderjoy is not that it is a shader playground, but how it radically bypasses this limit. It implements a "multiplexing" architecture. Instead of creating a WebGL context for every preview pane, shaderjoy creates exactly one hidden WebGL context.

It renders a specific shader to a hidden framebuffer, extracts the raw pixel data using gl.readPixels, and then paints those pixels onto an abundant, cheap 2D canvas using ctx.putImageData. It is a masterclass in mechanical sympathy.

The readPixels pipeline: One WebGL instance does the heavy lifting for an infinite number of 2D canvases.

The Dirty-Flag Scheduler

Most WebGL applications run a continuous, battery-draining requestAnimationFrame loop. They demand maximum GPU utilization even when nothing on the screen is changing. shaderjoy treats rendering more like a text editor.

A close-up of a heavy brass mechanical switchboard. One metal lever is flipped upward, engaging a single gear tooth, while the massive engine behind it remains still.
The dirty-flag approach: engaging the engine only when necessary.

It utilizes a "dirty-flag" pattern. Renders are only scheduled when the user types code or interacts with the interface. This targeted execution saves significant laptop battery life, contrasting sharply with the continuous, wasteful loops of typical graphics applications.

The Anti-Framework Architecture

Zooming out to the codebase itself reveals zero NPM production dependencies. There is no Webpack, no React, and no virtual DOM. The entire application logic resides in a single index.ts file.

Hedcut portrait of Cheng Lou, creator of shaderjoy.

Instead of relying on a framework for layout, shaderjoy performs batched DOM reads to calculate layout grids mathematically. This manual orchestration avoids layout thrashing while simultaneously managing 16 CodeMirror instances. It is the "anti-framework" approach to high-performance browser UIs.

StrategyContext CountGPU UsageCPU OverheadScalability Limit
Standard WebGL Grid1 per frameHighLowFails at ~16 contexts
Shadertoy (Classic)1 totalHighLow1 full-screen view
shaderjoy1 hiddenLowHigh (readPixels)Infinite 2D canvases