Publishing gate

Run the WebGL scene health check

Use the checklist to score assets, camera behavior, rendering, mobile performance, loading states, and fallback content before publishing.

Run Scene Check Compare lab tools

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
Open the examples gallery

Do not render unchanged scenes forever

A continuous loop spends battery even when the page is offscreen or visually static.

Fragile pattern
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();
Testable pattern
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.

SignalReview trigger
Device pixel ratioAbove 2
Texture dimensionsLarger than the visible camera need
Repeated geometryOne draw call per identical object
AnimationContinues while offscreen or hidden

Measure before removing visual features

Change one cost center and repeat the same interaction on the same viewport.

  1. Record renderer.info draw calls and triangles.
  2. Record texture dimensions and compressed transfer size.
  3. Test the 390 x 844 viewport with pixel ratio capped.
  4. 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.

Related guides