Optimizing 3D Model Loading in Three.js
Three.js makes it easy to put a 3D model on a page, and just as easy to ship one that takes eight seconds to appear and jerks as it loads. Getting a model on screen quickly, and keeping it smooth, is a craft of its own.
Table of contents:
The load waterfall is where users leave
Most Three.js performance complaints are really loading complaints. A visitor lands, sees a blank canvas or a frozen spinner, and leaves before your carefully lit model ever appears. The fix starts with the asset pipeline rather than the render loop. It comes down to what you download, in what order, and how much the browser has to decode before it can show the first frame.
Convert models to glTF with Draco or Meshopt geometry compression, which can shrink mesh payloads by 80 to 90 percent. Serve them with correct cache headers, and decode on a worker thread so the main thread stays free to render your loading state smoothly.
It helps to think of the whole experience as a sequence of gates. First the HTML and critical CSS arrive and the page becomes visible. Then the Three.js runtime loads and initialises. Then the model and its textures download, decode and upload to the GPU. Only after all of that does the first rendered frame appear. Each gate is a place a user can abandon, so the goal is to make the early gates instant and to show honest progress through the later ones. A lightweight poster image or a low-poly placeholder, shown while the full model streams in, keeps the canvas alive and buys you the seconds the download needs.
Compress geometry and textures aggressively
Draco compression handles geometry. For textures, use KTX2 and Basis Universal so images arrive as GPU-ready compressed blocks rather than PNGs the browser has to decode and re-upload. Together they attack the two heaviest parts of any glTF file at once.
- Run gltf-transform or gltfpack in your build to compress, dedupe and prune unused data.
- Resize textures to their actual on-screen size, since a 4K map on a thumbnail is wasted bandwidth.
- Strip unused animations, cameras and morph targets the exporter left behind.
Reuse geometries, materials and lights
Three.js will let you create a new material per object, and each unique material is another shader program and another draw call. Share a single material instance across objects that look the same, reuse geometry through InstancedMesh for repeated elements, and keep the light count low. Every additional dynamic light multiplies fragment-shader cost across the whole scene.
Bake lighting into textures where the scene is static. A baked lightmap gives you rich, soft global illumination for the cost of one extra texture lookup, instead of paying for real-time lights on every frame.
Tame the render loop
A naive continuous render loop draws every frame even when nothing changes, pinning the GPU and draining the battery. Render on demand, only when the camera moves, an animation plays, or state changes, and you reclaim large amounts of idle power. Pause rendering entirely when the canvas scrolls out of view using an IntersectionObserver.
Clamp the device pixel ratio, drop antialiasing on low-end devices in favour of a cheaper post-process pass, and prefer simpler materials such as MeshStandardMaterial when the visual difference is negligible.
Profile on the hardware users actually own
A scene that runs at a smooth 60fps on a development laptop can struggle on a three-year-old phone, and that phone is what much of your audience is holding. Profile early on representative mid-range hardware, using the browser performance timeline to separate GPU time from CPU and script time. The two bottlenecks have different cures, and guessing wastes effort.
Watch three numbers as you work: frames per second under real interaction, GPU memory while navigating between scenes, and time to first meaningful frame on a cold load. Improving one at the expense of another is easy to do by accident, so keep all three in view throughout development.
Set a hard performance budget before development starts, covering maximum bundle size, a texture memory ceiling, and a target frame time. A budget turns vague ambitions into concrete decisions the whole team can hold, and it stops a beautiful scene from quietly becoming one that no visitor can actually run.
Clean up, or leak
WebGL resources are not garbage collected the way JavaScript objects are. Swap a model without disposing the old geometry, material and textures and you leak GPU memory until the context is lost and the whole scene goes black. Call dispose() on everything you remove, and watch the GPU memory graph while navigating between scenes.
We apply this rigor to every interactive build we ship. For a 3D experience that is genuinely fast, see our web engineering and software capabilities, or talk to the studio about your project.