💡 Reference · Contributor Guide
📅 July 2026 ⏱ Reference 🎯 Pre-merge QA

Simulation Launch Checklist

A working checklist every contributor should run through before a new simulation ships on this site: frame-budget math, DPR and resize handling, WebGL context-loss recovery, memory disposal, accessibility, cross-device QA, and the metadata that makes a page discoverable. Treat every unchecked box below as a merge blocker.

1. Performance Budget

Before writing a single shader, decide the frame budget. On a 60 Hz display you have 16.6 ms per frame; on a 90 Hz mobile panel that shrinks to 11.1 ms. The renderer, your simulation step, and the browser's own compositing all share that window.

Frame budget: T_frame = 1000 / fps_target [ms]
60 fps → 16.66 ms    90 fps → 11.11 ms    120 fps → 8.33 ms

Budget split (rule of thumb for a single scene):
JS simulation step ≤ 30% of T_frame
GPU draw + shading ≤ 50% of T_frame
Headroom for GC / compositor / input ≥ 20% of T_frame

Draw-call cost estimate:
T_gpu ≈ n_draws · c_overhead + Σ(triangles_i / throughput)
where c_overhead ≈ 0.01–0.05 ms per draw call on integrated GPUs
Rule of thumb: if a simulation can't hold 30 fps on a mid-tier Android phone (e.g. Pixel 6a class) with the default settings, either lower the default quality preset or gate the heavy path behind a "high quality" toggle that defaults off.

2. Resize and Pixel-Ratio Handling

Every simulation must respond correctly to window resize, container resize (if embedded in a non-fullscreen panel), and orientation change on mobile — without leaking listeners or stretching the render target.

function setupResize(renderer, camera, container) {
  const resize = () => {
    const width  = container.clientWidth;
    const height = container.clientHeight;
    const dpr = Math.min(window.devicePixelRatio, 2);
    renderer.setPixelRatio(dpr);
    renderer.setSize(width, height, false); // false = don't touch CSS size
    camera.aspect = width / height;
    camera.updateProjectionMatrix();
  };
  const ro = new ResizeObserver(resize);
  ro.observe(container);
  resize(); // run once on init
  return () => ro.disconnect(); // cleanup handle
}

3. WebGL Context-Loss Recovery

Mobile GPUs and background tabs reclaim WebGL contexts under memory pressure. Every simulation must survive this without a white screen or console spam — the user just sees the scene reappear.

canvas.addEventListener('webglcontextlost', (event) => {
  event.preventDefault(); // required, or the context won't be restorable
  cancelAnimationFrame(rafHandle);
  console.warn('[sim] WebGL context lost — pausing render loop');
}, false);

canvas.addEventListener('webglcontextrestored', () => {
  // Textures, geometries, and programs must be re-uploaded —
  // three.js does this automatically for objects still in the scene graph,
  // but any manually-cached GL resources need manual re-init here.
  initRenderTargets();
  renderLoop();
}, false);

4. Memory and Disposal

Geometries, materials, and textures hold GPU buffers that are not garbage-collected by simply dropping JS references — they must be explicitly disposed, or repeated navigation to the same simulation leaks VRAM until the tab crashes.

function disposeScene(root) {
  root.traverse((obj) => {
    if (obj.geometry) obj.geometry.dispose();
    if (obj.material) {
      const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
      for (const mat of mats) {
        for (const key in mat) {
          const value = mat[key];
          if (value && value.isTexture) value.dispose();
        }
        mat.dispose();
      }
    }
  });
}

// On teardown / route change:
cancelAnimationFrame(rafHandle);
disposeScene(scene);
renderer.dispose();
renderer.forceContextLoss();
controls?.dispose?.();
Common leak: forgetting to dispose render-to-texture targets (WebGLRenderTarget) used for post-processing passes — these hold full-resolution GPU textures and are easy to miss because they never appear in the visible scene graph.

5. Input and Interaction

6. Accessibility

Contrast ratio: CR = (L1 + 0.05) / (L2 + 0.05)
where L1 = relative luminance of the lighter colour, L2 = of the darker, and L1 ≥ L2
WCAG AA: CR ≥ 4.5 (normal text), CR ≥ 3.0 (large text ≥ 18pt or 14pt bold)

7. Cross-Browser and Device QA

Manually verify the simulation on the matrix below before merging. Automated Lighthouse/CI checks catch regressions, but they do not catch broken drag interactions or context-loss bugs.

Target Why it matters Watch for
Chrome / Edge (desktop) Largest desktop share, best DevTools profiling Baseline — should be flawless
Safari (desktop, macOS) Different WebGL/shader compiler, stricter power limits Shader compile errors, colour-space shifts
Safari (iOS) WebKit-only engine on iPhone/iPad, tight memory ceiling Context loss on backgrounding, touch-scroll conflicts
Chrome (Android, mid-tier) Represents the median real-world device, not your dev laptop Frame rate, thermal throttling after ~2 min
Firefox (desktop) Different WebGL driver stack Extension support gaps, float-texture precision
Narrow viewport (<380px width) Small phones, split-screen layouts Overlapping UI, unreadable overlay text

8. SEO and Metadata

Every new page ships with correct, unique metadata — copy-pasted boilerplate with the wrong title/description is worse than none.

9. Final Pre-Merge Checklist

A condensed version to paste into a pull request description. If any item is not applicable, say so explicitly rather than omitting it.

// Simulation launch checklist — pre-merge
const checklist = [
  'Holds target fps on a mid-tier mobile device',
  'ResizeObserver + orientation change handled, no CSS size fight',
  'devicePixelRatio capped at 2',
  'webglcontextlost / webglcontextrestored handled + tested',
  'dispose() called for geometry, material, textures, render targets',
  'renderer.dispose() + forceContextLoss() on unmount',
  'Pointer Events used; touch-action set correctly',
  'prefers-reduced-motion respected',
  'Canvas has an accessible name; overlay UI is keyboard-operable',
  'WCAG AA contrast on overlay text',
  'No console errors on load or interaction',
  'Title, description, canonical, hreflang, OG, Twitter, JSON-LD all set',
  'Manually tested on Chrome, Safari desktop, Safari iOS, Android Chrome',
];
console.assert(checklist.every(Boolean), 'Fix before merge');
Reviewers: treat this checklist as the minimum bar, not the ceiling. A simulation that passes every box here can still be rejected for being confusing, ugly, or scientifically inaccurate — this reference only covers the technical launch gate.