Article / Performance

Why Your Three.js Site Is Slow on Phones (and the Five Fixes That Matter)

It is almost never the framework. The five real reasons WebGL sites struggle on mobile, how to diagnose each one, and the free tools that fix them: glTF Report, gltf-transform, Squoosh, KTX2, and a phone from a drawer.

The message usually arrives a week after launch. The site is beautiful on the studio machines, the client loves it, and then someone opens it on a normal phone in a parking lot and everything falls apart. Stutter, heat, a fan you can hear, a tab that dies. The first instinct is to blame Three.js. In years of doing this work, I have never once found Three.js to be the problem.

A phone GPU is a remarkable thing running on a strict allowance: limited memory, limited bandwidth, aggressive thermal throttling. Desktop hardware forgives waste. Mobile hardware invoices you for it. Nearly every slow WebGL site I have audited comes down to five kinds of waste, and all five have straightforward fixes.

The invoice metaphor is literal, by the way. Waste on mobile converts directly into heat, heat converts into throttling, and throttling converts into the site feeling worse the longer someone uses it. The first minute is the best your unoptimized scene will ever run.

One: your models carry geometry nobody sees

CAD exports, sculpting tools, and marketplace models routinely arrive with ten to fifty times the triangles the screen can express. A product that fills half a phone screen occupies maybe four hundred thousand pixels. A two million triangle model behind it is pure cost with zero visual return.

Diagnose it first: drop your file into glTF Report, which shows triangle counts, texture weight, and where the bytes live. Then fix it with gltf-transform or gltfpack, which handle simplification, deduplication, and Draco or meshopt compression in one pass. I walked through the full pipeline, from raw CAD to web-ready files, in From STL to glTF.

Two: your textures are eating GPU memory alive

This is the one almost everyone misses, because it hides behind a healthy download size. A 2048 pixel JPEG might be 400 KB over the network, but the moment the GPU uses it, it decompresses to raw pixels: 16 megabytes for that single texture, more with mipmaps. Six materials with color, normal, and roughness maps, and you have quietly claimed hundreds of megabytes of GPU memory on a device that shares that memory with the operating system. That is what crashes mobile tabs.

Two separate fixes, for two separate problems. For download size, resize to what the surface actually needs and compress with Squoosh. For GPU memory, convert textures to KTX2 with Basis Universal compression, which stays compressed on the GPU itself, cutting that 16 MB to roughly 2. gltf-transform does it in one command, or use KTX-Software directly. For product sites with many materials, KTX2 is the single highest-impact change on this list.

Run the arithmetic once and the priority becomes obvious. Six materials with color, normal, and roughness maps at 2048 each: eighteen textures, sixteen megabytes apiece uncompressed, roughly three hundred megabytes of GPU memory with mipmaps before the scene draws a single frame. The same set in KTX2 lands near forty. That difference is the difference between running and crashing on a two year old phone.

Three: too many draw calls, too many lights

Every mesh with its own material is a separate conversation with the GPU, and conversations have overhead. Two hundred draw calls feel free on desktop and become the frame budget on a mid-range phone. Merge static geometry, use instancing for repeated objects, and be honest about how many materials the scene really needs.

Lights are the same story multiplied. Every punctual light adds real per-pixel cost. The premium look you are after almost always comes from image-based lighting anyway: one good HDRI environment from Poly Haven replaces three or four lights, costs less per frame, and looks better. Bake what never moves. Light dynamically only what must respond.

Instancing deserves a special mention inside this fix, because product scenes repeat geometry constantly: four identical legs, a row of rivets, the same riser under three loungers. Rendered naively that is dozens of draw calls carrying identical data. Instanced, it is one. The API asks for ten extra minutes of setup and pays for itself on the first frame.

Four: you are rendering more pixels than the screen has

Modern phones report device pixel ratios of three or higher. Render at full ratio and you are pushing nine times the pixels of a 1x display, through a mobile GPU, for sharpness nobody can perceive in moving content. Cap the renderer at a pixel ratio of two, and consider dropping to 1.5 during heavy animation. Nobody has ever emailed me about a hero scene being insufficiently sharp mid-motion.

Post-processing multiplies the same waste. Bloom, depth of field, and ambient occlusion each re-render or re-sample the full frame. On desktop they are seasoning. On mobile they are often the whole meal budget. Feature-detect and serve the effects only where there is headroom for them.

Transparency is the stealth member of this category. Every transparent surface forces sorting and usually overdraw, and scenes accumulate transparent materials the way kitchens accumulate jars: nobody remembers adding them. Audit which materials genuinely need blending and you often reclaim milliseconds without changing a single visible pixel.

Five: everything loads at once

Performance is not only frame rate, it is the twelve seconds of white screen before the first frame. Load the hero asset first and let secondary content stream in behind it. Lazy load models for sections the user has not reached, the same pattern that keeps my Tenjam catalog configurator usable with eighteen products. Show something honest during the wait: a real progress state tied to actual bytes, not a fake spinner.

Perceived speed also has a cheap trick that works every time: draw the environment first. A scene that fades in its backdrop within the first second feels fast even while the hero model streams, because something answered the click. Blank canvases make people check their connection. Backdrops make them wait politely.

The sixth fix: adapt instead of assuming

The five fixes share an assumption: one build serving every device. The stronger pattern is adaptive quality. Measure what the device can actually do, then serve the tier it earns: full effects and resolution on strong hardware, trimmed post-processing and a capped pixel ratio in the middle, and a minimal but dignified version at the floor.

The measurement can be as simple as timing the first hundred frames and downgrading when the average slips, or as blunt as reading device memory and screen size up front. What matters is that the decision happens at runtime on real hardware, not at design time on a machine your visitors do not own. And the floor tier deserves real design attention. Trimmed does not mean broken, it means the same product with fewer luxuries.

How to actually diagnose

Fix in the order of evidence, not the order of blog posts. The waste hierarchy differs per project: a configurator usually bleeds through textures, a scroll experience through overdraw and post-processing, a data visualization through draw calls. Twenty minutes of measurement saves a week of optimizing the wrong thing.

  • glTF Report for asset audits before anything else. Most problems are visible there in thirty seconds.
  • Spector.js to capture a frame and see every draw call and texture the GPU touched.
  • renderer.info in Three.js itself: live counts of draw calls, triangles, and textures, one console log away.
  • Chrome DevTools performance panel with CPU throttling on, because your machine is lying to you.
  • A real, cheap, old phone. Mine lives in a drawer and has killed more bad ideas than any profiler.

And measure at the right moment. Phones throttle under heat, so a scene that holds sixty frames for the first thirty seconds can settle at forty once the chip warms up. Let the device run for two minutes before you trust any number it gives you.

The frame budget is not negotiable: sixteen milliseconds at 60fps, and thermal throttling will steal some of it after the first minute. The good news is that the budget is generous once you stop paying for waste. Phones run genuinely beautiful real-time work today, and the sites that fail are not failing because mobile GPUs are weak. They are failing because nobody sent the scene an invoice until launch week. And no, a newer rendering API will not save you: a heavy scene is slow in every API.