A WebGL quality governor that measures its own limits
A WebGL scene can start smoothly and become expensive when the camera enters a denser area. It can also stutter because the main thread is busy, the tab is throttled, or a shader is compiling. A quality governor needs to reduce work without pretending that every long frame identifies the same bottleneck.
The Lab uses a small governor to change rendering resolution. Its settings are specific to that scene. There is no single shared “high, medium, low” implementation across every project on this site.
Measure a symptom, then investigate its cause
A GPU name, viewport width, or maximum texture size can help choose an initial budget. None guarantees the frame time of a particular scene. Conversely, the interval between animation callbacks includes scheduling and CPU work as well as the consequences of rendering. It is not a direct measurement of GPU execution time.
If lowering resolution improves pacing, pixel work is a plausible contributor. If it does not, inspect JavaScript, draw submission, allocation, loading, and the frame cap. Avoid repeatedly degrading image quality to compensate for an intentional 30 FPS limit.
Read the actual governor
The current Lab smooths rendered-frame intervals with an exponential moving average. It bounds the interval sample at 100 ms and uses a weight of 0.05:
emaMs += (Math.min(100, elapsed * 1000) - emaMs) * 0.05; A weight of 0.05 has an e-folding response of about 19.5 samples: after that many updates, approximately 37% of an old error remains. At 60 updates per second that is about 0.33 seconds, not a two-second averaging window. At lower update rates it takes longer in wall-clock time.
| Decision | Current behavior |
|---|---|
| Step toward lower resolution | EMA above 30 ms, when the cooldown expires |
| Step toward higher resolution | EMA below 17 ms, when the cooldown expires |
| First decision / later cooldown | 120 / 240 eligible rendered frames |
| Wide desktop DPR caps | 1.5, 1.15, 1 |
| Intermediate or coarse-pointer caps | 1.25, 1, 0.9 |
| Phone caps | 1.1, 1, 0.85 |
The actual DPR is the smaller of the device DPR and the selected cap. A larger qIdx selects a smaller cap. Cooldowns are measured in rendered frames, so “240 frames” is four seconds at 60 Hz and eight seconds at 30 Hz.
There is a real limitation here: the phone renderer caps its cadence at 45 FPS. Its normal frame interval is already about 22.2 ms, above the 17 ms restoration threshold. That threshold does not provide a normal route back upward on that capped path. A future governor should relate restoration to the chosen frame budget or use a separate work-time signal; this article does not claim that improvement is already implemented.
Calculate what a DPR change buys
DPR affects two dimensions. For a CSS viewport of width w and height h, an approximate pixel count is w × h × dpr². Halving DPR quarters that count. It does not guarantee four times the frame rate because geometry, JavaScript, and fixed costs remain.
1280 × 720 at DPR 1.5: 1920 × 1080, or 2,073,600 pixels. One RGBA8 color buffer is about 7.91 MiB. At DPR 1, the same viewport has 55.6% fewer pixels.
Calculated from rounded-down buffer dimensions. This excludes depth, extra render targets, multisampling, mipmaps, and driver overhead. Values stay in your browser.
On an actual scene, use the renderer’s drawing-buffer dimensions when reporting allocated pixels. A composer may use additional targets at other resolutions. “Canvas memory” is not the same quantity as the total GPU memory consumed by the page.
Resize the whole pipeline consistently
renderer.setPixelRatio(nextDpr);
renderer.setSize(cssWidth, cssHeight, false);
camera.aspect = cssWidth / cssHeight;
camera.updateProjectionMatrix();
// If an EffectComposer is used, keep its buffers in sync too.
composer.setPixelRatio(nextDpr);
composer.setSize(cssWidth, cssHeight);
renderer.getDrawingBufferSize(bufferSize);
resolutionUniform.value.copy(bufferSize); This integration example assumes the renderer, composer, camera, vector, and uniform already exist. setSize receives CSS dimensions; do not multiply them by DPR and also call setPixelRatio, which would apply the scale twice. Its false argument avoids rewriting the canvas’s CSS size. Lowering DPR alone does not halve the displayed element.
Some effects need an inverse resolution, some their own target size, and some a logical viewport. Update each according to its shader’s contract. A blanket assignment to every uniform named “resolution” can be wrong.
Make quality changes deliberate
Resolution is one control. Shadow maps, geometry detail, raymarch steps, ambient occlusion, and bloom provide others. Change them in a planned order and inspect the visual effect. FXAA takes multiple samples and is not free; a color grade can also have substantial work. There is no reliable universal ranking that makes an unmeasured pass negligible.
Use separated upgrade and downgrade thresholds, a cooldown, and a bounded number of tiers. Ignore hidden-tab intervals and decide how startup stalls are handled. A time-based cooldown can make behavior more consistent across frame rates, but it must still exclude paused time if that is the intended policy.
Report evidence with its environment
Software-rendered browser checks are useful for startup, fallbacks, and finite rendering state. Their timings are not a forecast for a visitor’s phone. A performance report should name the browser, renderer, viewport, DPR, route, scene position, duration, and whether the page was foregrounded.
Record the selected quality, frame intervals, draw calls, and visible result together. If a lower tier still cannot produce usable interaction, offer the reading path. The purpose of a governor is to preserve a useful page, not to keep a canvas alive at any cost.
Sources & revision
Read the dated source excerpt. It includes original file paths, line numbers, and SHA-256 hashes for the version used in this guide. The public repository may contain a different revision.
28 Oct 2026: Replaced fictional shared tiers and device benchmarks with the current Lab settings. Corrected EMA timing, DPR sizing, and claims about post-processing cost.
These notes use AI assistance. Read the editorial method and corrections policy. Found an error? Send a correction with this article’s title.