home/field notes/Three.js
Three.jsPerformance

Taming WebGL: a Three.js hero that doesn't tank your LCP

A particle field is gorgeous and a great way to ruin Core Web Vitals. The exact recipe behind this site’s hero.

KB Kambiz BaghieFeb 20268 min

Why the pretty hero hurts

A full-screen Three.js scene is heavy in three ways at once: the script parse, the WebGL context creation, and a render loop that never stops. Mount it eagerly and it competes with your largest contentful paint for the main thread at the worst possible moment.

Lazy-init after first paint

  • Render the text hero with plain CSS first so LCP fires on real content.
  • Spin up the WebGL context after requestIdleCallback (or a short timeout fallback), well after first paint.
  • Fade the canvas in — the page never looks broken while it boots.

Cap the pixel ratio

Retina screens will happily ask for a 3× framebuffer. Clamp devicePixelRatio to ~2 and the fragment shader does a fraction of the work for a difference almost no one can see.

renderer.setPixelRatio(Math.min(devicePixelRatio, 2));

Stop rendering when no one's looking

An IntersectionObserver pauses the animation loop when the hero scrolls off-screen, and a visibilitychange listener kills it when the tab is backgrounded. Idle GPU, cool fans, happy battery.

That’s the whole recipe behind the hero on this very site — gorgeous on arrival, invisible in the performance trace.

Working draft. An outline of the real article — the full write-up is on the way. The notes below are genuine takeaways from shipping this in production.