Field notesRendering

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.

Values in the 28 October source snapshot
DecisionCurrent behavior
Step toward lower resolutionEMA above 30 ms, when the cooldown expires
Step toward higher resolutionEMA below 17 ms, when the cooldown expires
First decision / later cooldown120 / 240 eligible rendered frames
Wide desktop DPR caps1.5, 1.15, 1
Intermediate or coarse-pointer caps1.25, 1, 0.9
Phone caps1.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.