Cinematic, on a phone.
Ocean Sealink is a ship agency in Business Bay, handling port calls across the UAE, Oman and China. Their ask for the site fit in three words: cinematic, not laggy. They wanted a harbour at dusk behind every page, with a tanker riding in, a fleet at anchor and a skyline catching the light.
A 3D scene is the heaviest thing you can put on a web page. A rendering library, models, textures, shaders, and a render loop that would happily keep the browser's main thread busy forever. The usual result is a spinner, then a stutter, then a visitor who left during both. So we set one rule before writing any code: the scene never stands between a visitor and the words.
The harbour is decoration with a job. The headline is the job.
Nothing in front of the headline.
The first thing on screen is plain CSS. At build time, a script reads the opening frame of the scene and turns it into a gradient: the sky and the sea as colour bands, a soft glow where the sun sits on the horizon, a one-pixel horizon line. No image, no script, no preload. The headline paints over it straight away, and the largest thing on the page is text.
When the live scene is ready, it fades up over that gradient in 1.6 seconds with a slow push of the camera, so the page seems to come to life instead of swapping one picture for another. If the scene never arrives, a pre-rendered still takes over at six seconds, quietly, at low priority.
The scene lives on another thread.
A tiny script in the page head starts a Web Worker before the body has even parsed. The engine, three.js and all of it, downloads and boots inside that worker while the browser is still laying out text. The worker draws to an OffscreenCanvas, so the main thread keeps a 4.4 KB host and nothing else. Scrolling, taps and forms never wait for a frame to finish.
Older browsers that can't draw from a worker, like Safari on iOS 16, get a second path: the same engine on the main thread, built in small slices that hand control back to the browser between steps, and started only after the page has loaded. Phones that can do neither get the still pictures. Nobody gets a page that fights their thumb.
No models to download.
Every ship, tower and ridge in the harbour is built in code. The bulk carriers, the product tankers, the stepped deckhouse on the container ship, the setbacks and spires on the skyline: all of it is geometry generated in the worker, with textures painted onto a canvas there too. The only file the scene fetches besides its own code is a 3.7 KB map of where the land is.
That puts the whole harbour, three.js included, at 181 KB gzipped, cached for a year after the first visit. A single bought ship model can weigh more than that. Porting the scene from the design prototype roughly halved its draw calls without changing what it looks like, 543 to 254 per frame in one view, and when the client asked for more detailed ships, the new fleet came in cheaper to draw than the old one.
Same scene, every phone.
The usual trick is to guess how strong a device is and hand weaker ones an uglier scene. We tried it. Device hints lie: Brave on a OnePlus 13R reports five cores and 4 GB of memory, which is nowhere near the truth. So the guessing went, and every device that can run the scene now gets the same one, with the same reflections, shadows and light.
What adapts is the frame rate. The engine measures itself while it fades in, then watches for missed frames and GPU lag. When a phone runs short it steps down one rung at a time: shadows every other frame, a slower sky, fewer pixels, then 30 fps, then 20. It climbs back up after ten clean seconds. On that same OnePlus the scene went from 20 to 30 fps to a steady 60, at rest, while scrolling and through a four-minute soak.
Stop drawing when nobody is looking.
The cheapest frame is the one you never render. Switch tabs and the loop stops. Scroll until a solid section covers the harbour and the loop stops, then wakes 200 pixels before the scene shows again. When nothing in the scene is moving, nothing is drawn.
Moving between pages doesn't rebuild anything either. The scene survives navigation: one worker and its 40 compiled shader programs carried through five page changes in testing, the harbour still moving while the new page slides in. Visitors who ask their device for reduced motion get still pictures of the harbour, on purpose.
What the numbers said.
In lab runs, the headline painted in 0.65 seconds on desktop and between 0.83 and 1.46 seconds on a phone profile throttled to a slow network and a quarter of the CPU. Layout shift stayed at or under 0.01. No long tasks on the main thread. The home page weighs 268 KB on a phone before the engine, and the engine itself never sits in front of anything.
None of this is exotic. A worker, a gradient, code instead of models, a frame rate that bends instead of a scene that breaks. The hard part was refusing every idea that put something beautiful in front of the words, including three of our own.