Start with a budget, not a profiler panic
WebGL performance problems often appear late because the scene starts simple and becomes expensive one feature at a time. A small site should have a budget before the visual polish begins. Decide how many objects, draw calls, lights, texture sizes, and postprocessing passes are acceptable for the page's purpose.
A hero scene, a product viewer, and a full-screen tool deserve different budgets. A hero scene shares attention with text, navigation, and scrolling. A tool can justify more GPU work because interaction is the main experience. Write this down so every new effect has to earn its cost.
Watch draw calls and materials
Many small meshes with unique materials can cost more than one larger mesh with shared material. Imported models may contain dozens of nodes from the authoring tool. Inspect `renderer.info.render.calls`, mesh count, and material count. These numbers explain why a scene feels heavy even when the triangle count looks reasonable.
When possible, merge static geometry, reuse materials, and remove hidden authoring leftovers. If a viewer is intended for arbitrary uploaded models, expose these metrics rather than silently accepting every asset as production-ready.
Texture size is a content problem
Huge textures are easy to miss because the model still loads. A 4096 pixel texture may be fine for a close-up product render but excessive for a small card preview. Compression and resizing often deliver more improvement than rewriting code. If the page targets mobile users, texture memory matters as much as JavaScript size.
Use the smallest texture that survives the intended camera distance. For many web previews, 1024 or 2048 pixel maps are enough. Save 4K maps for scenes where the user can zoom close enough to benefit.
Stop rendering when nothing changes
The simplest render loop redraws every frame forever. That is fine for animated scenes, but wasteful for static viewers. If the object is still, render on controls change, resize, or UI updates. If the scene is below the fold, pause animation with IntersectionObserver. If reduced motion is enabled, remove nonessential loops.
Performance is part of trust. A site that makes a laptop fan spin for a decorative hero scene feels careless. Keep the WebGL work proportional to the user's benefit.
Use the examples as isolated cost probes
The gallery separates a mesh, a point cloud, and a shader plane so a regression can be associated with one rendering pattern.
- One pattern per canvas
- Offscreen work can be paused
- Mobile layout remains readable
Do not render unchanged scenes forever
A continuous loop spends battery even when the page is offscreen or visually static.
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();let dirty = true;
function frame() {
if (visible && dirty) renderer.render(scene, camera);
dirty = controls.update();
requestAnimationFrame(frame);
}
frame();Pre-publish budget signals
These are review triggers, not universal performance guarantees.
| Signal | Review trigger |
|---|---|
| Device pixel ratio | Above 2 |
| Texture dimensions | Larger than the visible camera need |
| Repeated geometry | One draw call per identical object |
| Animation | Continues while offscreen or hidden |
Measure before removing visual features
Change one cost center and repeat the same interaction on the same viewport.
- Record renderer.info draw calls and triangles.
- Record texture dimensions and compressed transfer size.
- Test the 390 x 844 viewport with pixel ratio capped.
- Pause offscreen rendering and compare the browser performance trace.
Basic render metrics
function logRenderInfo(renderer) {
const { render, memory } = renderer.info;
console.table({
calls: render.calls,
triangles: render.triangles,
geometries: memory.geometries,
textures: memory.textures
});
}
setInterval(() => logRenderInfo(renderer), 2000);
Sources and further reading
Update record
- : Rewritten around a reproducible demo, explicit test record, code comparison, and known platform limits.