Hoe optimaliseer je 3D-webperformance: Three.js, R3F en de lui-geladen hero
Een 3D-hero ziet er geweldig uit in een productlaunch-video en geweldig in je Lighthouse-auditfalen. Dit is hoe we een Three.js-homepage hebben uitgerold die de initiële bundle lean houdt en de interactieve scène warm.
De 3D-homepage van 2run.be is gebouwd op Three.js met React Three Fiber. De eerste versie van die pagina voegde 600 KB toe aan de initiële bundle en duwde Lighthouse-mobile-Performance onder de 60. De huidige versie levert de 3D-scène pas nadat de gebruiker in de buurt ervan heeft gescrold, en weegt op het kritieke pad een tiende van wat de eerste versie deed. Dit is hoe we daar zijn gekomen.
Wat zit er in een 3D-webscène
Een typische R3F-scène-grafiek trekt mee:
- Three.js zelf (~150 KB gzipped kern, meer met controls, loaders, postprocessing).
- React Three Fiber (~30 KB) en Drei (~50 KB+ afhankelijk van de gebruikte helpers).
- De geometry- en material-loaders die je werkelijk gebruikt.
- De shader-programma's, als je custom shaders schrijft.
Een conservatieve baseline is 250-350 KB gzipped voor een niet-triviale scène. Een zwaardere scène met physics, postprocessing en custom shaders kan boven de 600 KB uitkomen. Dat getal is het hele verhaal voor performance: het concurreert om het kritieke-pad-budget met de rest van de pagina.
Twee benaderingen
Benadering A: meeleveren op het kritieke pad
Je commit je eraan dat de 3D-scène deel uitmaakt van de eerste paint. De volledige scène, inclusief geometry-loaders en controls, landt in de initiële bundle. De gebruiker ziet de 3D-scène zodra de HTML aankomt. Het first-paint-verhaal is het 3D-verhaal.
Dit werkt als:
- De rest van de pagina werkelijk minimaal is (een portfolio-hero, een landingspagina).
- De 3D-scène de value proposition is — de scène verkopen is het product verkopen.
- Mobiele gebruikers in je doelmarkt het meeste op wifi zitten.
Het werkt niet als:
- De 3D-scène decoratief is, niet functioneel.
- De pagina meer inhoud heeft (tekst, CTAs, een formulier) dan alleen de scène.
- Mobiele LCP deel uitmaakt van je Core Web Vitals-commitment.
Benadering B: lui-laden na eerste interactie
Je stelt de 3D-scène uit tot een next/dynamic-import met ssr: false, en triggert de load pas wanneer de gebruiker in de buurt van de hero scrolt of na een kleine vertraging. De initiële bundle draagt de 3D-payload nooit. De scène verschijnt wanneer de gebruiker die gaat zien.
Dit werkt als:
- De hero-inhoud boven de fold op zichzelf kan staan (een afbeelding, een gradiënt, een typografische uitspraak).
- Het first-paint-verhaal de beloften van het merk zijn, niet de 3D-scène.
- Je een
IntersectionObserver- of scroll-listener hebt om de load te triggeren.
Dit is de benadering waarop we zijn geland voor de 2Run-homepage. De trade-off:
- Pro: Initiële bundle daalt met 600 KB. Lighthouse-mobile-Performance springt van onder de 60 naar boven de 90.
- Con: De gebruiker ziet ~0,5 s een placeholder voordat de scène bevolkt. Met een goede plaatsing van de triggerzone voelt dit aan alsof de scène "oprijst" in plaats van "ineens verschijnt".
De implementatieschets
Het patroon, teruggebracht tot de essentie:
- Een
useInView-hook die een boolean teruggeeft wanneer de target-ref in de viewport is. - Een
next/dynamic-import voor de zware scènecomponent metssr: falseen een lichte ladende skeleton. - De hero-sectie rendert de typografie en CTA gretig, en mount de scène dan voorwaardelijk wanneer
inViewop true klapt.
Een subtiele gotcha: de scènecomponent moet ook een reduced-motion-fallback accepteren. Gebruikers die in het besturingssysteem reduced motion hebben aangevraagd, krijgen de typografie-hero zonder de 3D-scène, en Lighthouse-Performance is niet de enige metriek die ertoe doet.
Mobiel en de laagste klasse
De eerlijke waarheid over mobiele 3D: een middenklasse Android-telefoon bij 30 °C accu-throttling kan nog steeds niet tippen aan een desktop-browser bij dezelfde scènetComplexiteit. Twee paden:
- Detecteren en degraderen. Gebruik een
gl-capaciteitscheck (renderer.getContext() en een kleine benchmark) en serveer een lower-poly scène op zwakkere apparaten. Beste UX, kost engineering-tijd. - Onvoorwaardelijk renderen en de kosten accepteren. Prima voor B2B-kopers op bedrijfsbeheerde apparaten. Niet prima voor consumentenpubliek.
We kozen het eerste pad. De detectielogica draait eenmaal per sessie en het resultaat wordt in sessionStorage gecached. De fallback-scène gebruikt lower-poly geometry en laat de postprocessing-pass vallen.
Wat niet in dit verhaal zit
- Server-side rendering van 3D-scènes. Er is geen SSR voor Three.js — de canvas heeft een DOM-context nodig. SSR-stubs voor een 3D-scène zijn meestal decoratief en voegen SSR-complexiteit toe voor beperkte winst.
- GPU-shader-precompilatie. Echte wins, maar tenzij je een custom postprocessing-laag uitlevert, is de browser JIT goed genoeg.
- 3D op de LCP zelf. 3D als LCP-element targetten is het pad naar een snelle initiële paint voor een 3D-portfolio en een trage LCP voor een inhoudszware pagina. Kies welke je waar wilt hebben.
De take-away
Behandel 3D-webscènes zoals je elke andere zware feature behandelt: profileer de bundle, stel uit wat je kunt, render de juiste klasse op het juiste apparaat, en eigenaar de trade-off expliciet. Een 3D-scène die Core Web Vitals faalt, is geen creatieve overwinning — het is een launch-site die niet rankt.
