Blog Details
Home/Blog Details

A 3D city that fits in a phone browser's frame budget
Written by Abdulbasit Abdulalim| 5 min Read
Two projects, one constraint
A property gallery and a street game. The first shows a house; the second shows a city you drive a scooter through. Very different products, one shared problem: both have to run in a browser, on a phone, on a connection that is not good.
The phone constraint is not a detail. A browser 3D scene that runs on a development machine and stutters on a mid-range Android phone is a demo, not a product. And the "mid-range Android phone" is the device most of the people who would use these things are holding.
The budget, stated first
Before any modelling, the frame budget was written down: 16.7 ms per frame at 60 fps, and the renderer has to fit inside that on a device that is not a development machine. Everything after that is a negotiation with that number.
Three things eat a frame budget in a browser scene, and they need different answers:
- Draw calls. Every mesh, every material, every texture binding costs a call into the graphics driver. A thousand small objects is a thousand calls, and the driver - not your JavaScript - is the bottleneck.
- Triangles. Geometry that does not need to be on screen is still transformed, still clipped, still rasterised.
- Fill rate. Every pixel covered by a transparent, blended or overlapping surface is expensive. Shadows, effects and overdraw live here.
The answers
Chunk the world. The city is not one scene; it is a grid of chunks, and only the chunks near the player exist. Chunks load ahead of the player and are released behind them. This is not an optimisation so much as a requirement: a full city has to be divided into pieces before it can be delivered, streamed and freed at all.
Use level of detail, and use it honestly. Each object has a detailed version and a simple version, and the switch is by distance. The switch has to be invisible, which is the hard part - a building that pops from a box to a detailed model right in front of the player is worse than a building that was always a box. The practical rules that came out of it: switch in the direction of travel, switch behind the player rather than in front, and never switch something the player is close enough to be studying.
Merge the static geometry. Roads, pavements, kerbs and building shells do not move and do not need individual materials. Merging them into a handful of large meshes turns thousands of draw calls into tens. The city can then afford detail where detail is visible.
Do not render what the camera cannot see. Frustum culling, distance culling, and - for the interiors - not modelling the inside of buildings nobody can enter. Some of this is trivial and some of it is genuinely free performance, because the work you never ask for is the work you never pay for.
Load the heavy parts after the first frame. The first frame is what the user is waiting for. Textures, environment maps and detail geometry can arrive after the scene is already on screen and stable, and the user sees a scene rather than a loading bar.
The property gallery, which is a different problem
A gallery does not need a city. It needs a room - and a room is much harder than it sounds, because a room is exactly where the user looks.
The approach that worked: build the room as a set of surfaces, light it with baked lighting rather than real-time shadows, and make the camera movement constrained and deliberate. A constrained camera is a design decision as much as a performance one - it means the scene only ever has to look good from a few angles, and that is a far smaller job than making every angle good.
The loading behaviour is the part that decides whether it feels expensive or cheap: the first view is available almost immediately, and detail fades in behind it.
The failure modes
The frame budget is spent before the scene is visible. A skybox at high resolution, a full-screen post-processing chain and a large environment map can each eat a millisecond or two before a single building is drawn.
Shadows are the classic mistake. A shadow map is a second render of the scene. On a phone, that doubles the cost of the thing you were already struggling to afford. Baked shadows and blob shadows under objects cost nothing and look nearly as good.
The device is not the one you tested on. The real constraint is the low end, and the low end is always worse than the device you have. The scene has to be profiled on the slowest supported device, not the fastest.
Textures are memory, and memory is failure. A phone that runs out of memory does not degrade gracefully - it kills the tab. Texture budgets and disposal are not a nicety.
What I would keep
- Write the budget down first. "16.7 ms" turns an open-ended argument about quality into a concrete trade.
- Reduce draw calls before triangles. The driver is usually the bottleneck, and merging geometry is cheap to do.
- Decide what the user can see, then only build that. Most scenes contain geometry that exists because it was easy to model.
- Test on the slowest supported device, not the fastest one you own.
The city in Streetverse and the gallery in Atrium are different products with the same discipline underneath: the phone is the target, the budget is written down, and everything that is not worth its milliseconds is not in the scene.


