When we started building Riya, our 3D host, she looked great on a desktop and fell apart on phones. The page reloaded on iPhones, the model took forever to download, her voice stayed silent in Safari, and the chat box disappeared behind the keyboard. Each problem had a different cause. This is what we learned fixing them, in the order you're likely to meet them.
1. The page reloads on iPhone: GPU memory
Symptom: the avatar loads, then the page reloads by itself, sometimes in a loop, or the 3D view goes blank. It happens on iPhones and iPads, rarely on Android or desktops.
Cause: iOS Safari drops a page's WebGL, and often the whole page, when it uses more graphics memory than iOS allows, which is a few hundred megabytes. It doesn't tell you the limit or even which iPhone the visitor has. Textures are the main cost: one 2048 × 2048 texture takes about 16 MB of GPU memory uncompressed, and a character has dozens of them (skin, eyes, hair, clothes, each with colour, normal and roughness maps).
What worked:
- A lighter model for every iOS device, not only "small" ones, since you can't tell them apart. We keep the face, hair, eyes and teeth at 1024 pixels (that's what people look at) and scale the body and clothes down to 512. The whole character fits in about 110 MB.
- Dropping empty morph targets at load. Our model had 916 blend shapes, and 646 of them were empty on some meshes, all of them using memory.
- Recovering instead of crashing. Listen for
webglcontextlost. When it fires, reload with the light model, then at minimal quality, and if even that fails, show a still picture with the chat still working.
2. The model is huge: download size
Symptom: a long loading bar on mobile data, and a deploy that fails because a file is too large for the host.
What worked: compressing the glTF file with gltf-transform: Meshopt compression for geometry and animation, WebP textures, and resizing textures to what each part actually needs. Riya went from 136 MB to 14 MB.
The trap: our first compression broke an arm animation, because the optimiser resampled the animation curves and removed keyframes it considered redundant. Compressing with Meshopt but without resampling fixed it. If a limb suddenly twists after optimising, check this first.
3. No sound in Safari: the audio rules
Symptom: the avatar moves her lips in silence in Safari on iPhone and Mac, while Chrome is fine.
Cause: browsers block audio that starts without a user gesture, and Safari is the strictest. On iOS, an audio element generally has to be started inside a tap before it may play later on its own.
What worked:
- Start the voice from a tap: a "Talk to me" or "Sound on" button, never autoplay with sound.
- Unlock the audio element inside that tap: call
play()on it right away, then pause it. Later sentences, started by your code, will play. - Keep the voice in sync with its own clock. Drive the lip sync from the audio's
currentTime, so a delayed start on one browser doesn't leave the mouth ahead of the voice. More in our three.js lip sync guide.
4. The chat box hides behind the keyboard: mobile layout
Symptom: on iPhone the input field slips under the keyboard or the browser's toolbar, and tapping it zooms the whole page in.
What worked:
- Dynamic viewport units (
100dvhinstead of100vh), which follow the browser's toolbars as they show and hide. - Safe-area insets (
env(safe-area-inset-bottom)) so nothing sits under the home indicator. - A font size of at least 16 px in inputs. Below that, iOS zooms in when the field is focused.
5. Slow on some laptops: no graphics card
Symptom: the avatar runs at a few frames per second on certain Windows laptops, and in Chrome 137 and later can stop showing at all.
Cause: when hardware acceleration is off, or the laptop's graphics driver is blocklisted, the browser draws WebGL on the CPU. Since Chrome 137, it often doesn't fall back to the CPU at all, and WebGL is simply unavailable.
What worked:
- Detect it (the WebGL renderer string names SwiftShader or the Microsoft Basic Render Driver) and switch to a CPU-friendly model: full detail on the face, a quarter of the triangles and small textures elsewhere, and simpler shading. For us that meant about 18 fps instead of 6.
- Adapt the quality as you go. Start from a level guessed from the device, then step down while the frame rate stays under about 50 fps.
- Without WebGL, show a picture, and keep the conversation. Add short instructions for turning the graphics card on.
A checklist before launch
- Test on a real iPhone and a mid-range Android phone, not only the desktop browser's phone simulation.
- Keep total texture memory on iOS around 100 MB, and the download in the low tens of megabytes.
- Handle
webglcontextlostand have a still-picture fallback. - Start the sound from a tap, and unlock the audio element there.
- Use
dvh, safe-area insets, and 16 px inputs. - Load the 3D after the page's text, so the page itself stays fast for visitors and search engines.
Every Lobbyface host is built to these rules, which is why they run on ordinary phones. If you're planning a host of your own, see how to add an AI avatar to your website.