Building with WebGL and Three.js is intoxicating: you can ship real-time 3D to anyone with a browser. Shipping it in production—across iPhones, low-end Android, corporate laptops, flaky GPUs, and ad-blocked networks—is where the romance ends and the engineering begins.

This post is a field guide for developers and creative tech teams who want their Three.js work to survive contact with real users.

Start with a performance budget (or you’ll guess forever)

Most WebGL failures in production aren’t “bugs,” they’re unmet budgets: too many draw calls, too much overdraw, too many pixels, too much shader work, too much memory.

A workable baseline budget for interactive scenes:

  • Frame time: 16.6ms (60fps) target; accept 33ms (30fps) on mobile.
  • Draw calls: aim < 150 desktop, < 80 mobile.
  • Triangles: wildly scene-dependent, but < 500k visible is a good starting point.
  • Texture memory: treat 256–512MB as a soft ceiling on desktop; much less on mobile.

Instrument early:

  • Use renderer.info (programs, geometries, textures, draw calls).
  • Add a tiny in-app overlay to log FPS, draw calls, GPU tier, and memory.
  • Test fill rate: large transparent overlays + postprocessing can be more expensive than geometry.

The opinionated take: if you can’t explain where the milliseconds go, you don’t own the scene yet.

Choose your rendering approach deliberately

Three.js makes it easy to pile on effects. In production, you need a clear “why” for each.

  • Forward rendering is simpler and often faster for modest light counts.
  • Deferred rendering isn’t built-in as a first-class feature; simulating it via passes can balloon bandwidth.
  • Postprocessing (bloom, DOF, SSAO) is usually the biggest cost. Downsample aggressively.

Practical tactics that ship well:

  • Render at dynamic resolution: reduce pixel ratio when FPS drops.
  • Limit expensive passes on mobile via feature flags.
  • Prefer baked lighting (lightmaps) when the scene is mostly static.
  • Use fog and tone mapping as “cheap art direction” before reaching for heavy effects.

Asset pipeline: treat it like software, not “art drop”

Your runtime performance is mostly decided in your DCC tools (Blender, Maya) and export pipeline.

Production rules worth enforcing:

  • Standardize on glTF / GLB. It’s the right choice 95% of the time.
  • Compress geometry with Draco or Meshopt (Meshopt often wins on decode speed).
  • Compress textures with KTX2 / BasisU (GPU-native formats matter).
  • Generate LODs for large scenes and hero assets.
  • Merge meshes when it reduces draw calls, but don’t over-merge if it kills culling.

Pipeline checklist:

  • Ensure consistent unit scale and pivot conventions.
  • Enforce texture caps (e.g., max 2048 for mobile) and power-of-two when needed.
  • Bake tangents only if you need normal mapping; otherwise skip.
  • Run an automated “asset linter” in CI: file size thresholds, missing textures, too many materials.

If artists can unknowingly ship a 40MB normal map, they will. Guardrails beat Slack messages.

Materials, lights, and shaders: keep it boring on purpose

PBR realism is expensive when you combine: many lights, high-res environment maps, shadow maps, and layered transparency.

What works in production:

  • Prefer one directional light + environment map for most scenes.
  • Keep shadow-casting lights to a minimum; tune shadow map size per device.
  • Avoid lots of unique materials. Material diversity is draw-call poison.

When you do need custom shaders:

  • Wrap them with onBeforeCompile or ShaderMaterial, but keep variants controlled.
  • Build a shader define strategy (feature toggles) to avoid recompiling every frame.
  • Watch for precision issues on mobile (mediump artifacts, banding).

A common production failure: shipping a gorgeous desktop shader that quietly black-screens on a mid-range Android GPU.

Loading, streaming, and perceived performance

Your first 3 seconds decide user retention.

  • Use progressive loading: show something interactive quickly, stream the rest.
  • Split scenes into chunks; load “above the fold” content first.
  • Use LoadingManager but don’t lie with fake progress bars—tie progress to bytes.

Caching matters:

  • Serve assets with long-lived cache headers and content hashing.
  • Consider a service worker for offline-first experiences or repeat visits.
  • Preload critical textures and environment maps.

Also: handle failure. Users lose connection. Corporate proxies strip range requests. Your loader should retry gracefully and fall back to lower-quality assets.

Cross-device stability: test the ugly matrix

WebGL in production is less about correctness and more about compatibility.

You need to test:

  • iOS Safari (WebGL quirks, memory pressure, background tab behavior)
  • Low-end Android (thermal throttling, precision limits)
  • Windows + integrated graphics (driver issues)
  • High-DPI laptops (pixel ratio costs)

Hardening tactics:

  • Detect capabilities (WebGL1 vs WebGL2, extensions) and choose feature tiers.
  • Clamp device pixel ratio: Math.min(window.devicePixelRatio, 2) is a sane default.
  • Handle context loss: listen for webglcontextlost / webglcontextrestored.
  • Be conservative with render targets and postprocessing on mobile.

If your product matters, include a “safe mode” toggle that disables heavy effects.

Debugging and observability: ship the tools with the app

DevTools and Spector.js are great—until the bug only happens on a client’s locked-down machine.

Make production diagnosable:

  • Log renderer and GPU info (vendor/renderer strings when available).
  • Capture scene stats (draw calls, texture count, geometries, FPS trend).
  • Add a hidden “debug panel” behind a query param.
  • Record errors from loaders and shader compilation failures.

For rendering bugs:

  • Use Spector.js locally to inspect draw calls and textures.
  • Add a “wireframe/overdraw view” mode for internal QA.

Opinionated but true: without observability, you’ll blame WebGL for problems you created.

CI, regressions, and keeping quality over time

Creative code rots fast because visual changes mask performance regressions.

Add automation:

  • Lighthouse (for loading) plus custom perf checks (FPS over 10 seconds on a test scene).
  • Visual regression tests: render a deterministic frame and compare pixels with a tolerance.
  • Bundle size budgets (especially for postprocessing and loaders).

Three.js also moves quickly. Pin versions, upgrade intentionally, and run a compatibility pass before shipping.

Conclusion: production Three.js is a discipline

Three.js is absolutely production-ready—but only if you treat real-time 3D like a product, not a demo. Set budgets, build an asset pipeline with enforcement, tier your features by device, and ship observability so you can debug reality.

The teams that win aren’t the ones with the fanciest shaders; they’re the ones who can deliver consistent visuals at a stable frame rate—everywhere.