Performance report
The 3D stays, the cost moves off the first load
The home page ships a real-time 3D scroll story. On a mid-range phone it used to tie up the main thread for seconds, and PageSpeed Insights couldn't finish testing it. These are the changes, the numbers, and what each one costs.
PageSpeed Insights, mobile
| Page | Score | LCP | TBT | CLS | FCP | Report | |
|---|---|---|---|---|---|---|---|
| Home/en-us | Before | Didn't finish (2 attempts: the renderer crashed, then the run timed out) | PSI ↗ | ||||
| After | 95 | 2.6 s | 80 ms | 0 | 1.7 s | PSI ↗ | |
| Product page/en-us/products/portva-140 | Before | 93 | 3.2 s | 40 ms | 0.007 | 0.9 s | PSI ↗ |
| After | 94 | 3.0 s | 50 ms | 0.007 | 0.9 s | PSI ↗ | |
| Collection/en-us/collections/all | Before | 99 | 2.0 s | 10 ms | 0 | 0.9 s | PSI ↗ |
| After | 98 | 2.4 s | 20 ms | 0 | 0.9 s | PSI ↗ | |
What the browser has to do (local build)
Home
- JavaScript
- 557 → 295 KB-47%
- Total transfer
- 1,018 → 438 KB-57%
- Images
- 23 → 23 KB
- Main-thread work
- 4,435 → 1,596 ms-64%
- TBT
- 522 → 55 ms-89%
Product page
- JavaScript
- 297 → 298 KB
- Total transfer
- 721 → 560 KB-22%
- Images
- 297 → 133 KB-55%
- Main-thread work
- 1,207 → 1,128 ms-7%
- TBT
- 45 → 48 ms+7%
Collection
- JavaScript
- 295 → 295 KB
- Total transfer
- 515 → 515 KB
- Images
- 102 → 102 KB
- Main-thread work
- 740 → 824 ms+11%
- TBT
- 55 → 52 ms-5%
What changed, and what it costs
- 01
three.js waits for the first interaction
The home hero shows the product render first. three.js, React Three Fiber and the model load on the first scroll, touch or mouse move — which is also when the story starts moving.
Trade-off: On a phone the live model replaces the poster about a second after the first touch. The poster is the same render, so the swap is a cross-fade, not a jump.
- 02
The canvas renders only while scrolling
The WebGL canvas used to draw 60 frames a second even on a still page. It now renders a frame only when the scroll timeline moves the model. On machines without a GPU (like PageSpeed's), the old loop never let the page go idle.
Trade-off: Any future idle animation has to request its own frames.
- 03
The buy area is in the prerendered HTML
The product view read ?variant= itself, which made the photo, title and price a dynamic hole: the LCP image was only discovered when that part streamed in. The static shell now renders the default variant, so the image preload sits in <head>, and the requested variant replaces it in the same response.
Trade-off: A deep link to another colour can show the default colour for a moment on a slow connection.
- 04
Images are staged
Only the photo you arrive on loads with the page. Other gallery photos and swatch colourways mount once the page is idle, then stay mounted so switching is instant. Thumbnails fetch at low priority.
Trade-off: Hovering a swatch in the first second can show the default colour until its image arrives.
- 05
Lifestyle scenes are pre-sized
The scene renders were served as 2560 px JPEGs (~180 KB) at every size. A script now writes 640/1080/1920 px WebP versions (6–27 KB) and the image loader picks the nearest one.
Trade-off: New renders need the script re-run (scripts/scene-variants.mjs).
How it was measured
- PageSpeed Insights, mobile: Lighthouse 13.6, emulated Moto G Power on simulated slow 4G, run from Google's servers (test location: Asia). Anyone can rerun it from the links below.
- “Before” is one run of the version that had been live for hours. “After” is the median of three runs (by LCP), taken after one extra run that warms Vercel's edge cache following the deploy.
- Bytes and main-thread time come from Lighthouse 12.8 on a local production build (median of 3), because they barely depend on the network.
What these numbers don't show
- On the product and collection pages the PageSpeed difference is within run-to-run noise: the same code measured LCP between 2.0 and 3.7 s on different runs, with the server answering in under 10 ms. Their gain is in what the browser downloads (product page images −55%), not in a provable LCP change.
- This is lab data. The store has no real traffic, so there is no field (CrUX) data to confirm it.
- Single PageSpeed runs vary by a few points; treat small differences as noise.
- The Liquid storefront sits behind the dev store's password, which PageSpeed can't pass, so it isn't in this table.
- Images come from Shopify's CDN; LCP on a real phone depends mostly on the network between the visitor and that CDN.
Code: before 993ba71, after 97e7b63.