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.
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.
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.
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.
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.
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.