0004 — Three.js WebGL2 PWA client
Date: 2026-11-09 · Status: accepted
Context
Mobile-first, web-first, 3D, with dense informational UI over one viewport. Renderer bytes are a first-load cost on mobile networks, and the renderer plus material model are expensive to reverse. Every figure below was measured or version-checked on 2026-11-09 in docs/research/client-stack.md.
Decision
Three.js 0.187.0 with WebGLRenderer (WebGL2), React 19.3.1, react-three-fiber 9.8.2 (drei 10.7.9 only for controls), Tailwind 4.3.5, Vite 8.3.7, TypeScript 7.0.2. One full-viewport fixed <canvas> at z-index: 0 with the UI as DOM in a sibling layer — never in-canvas. WebGPU sits behind a single createRenderer() seam. Installable PWA, no native wrapper.
- Bytes: 189 KB gzip for three with a full compressed-glTF pipeline, against Babylon's 694 KB and PlayCanvas' 540 KB — 3.7× and 2.9× smaller, measured with esbuild 0.28.2 +
gzip -9. Three is the only one under 200 KB. - React: r3f 9.8.2 (peer
react >=19 <19.4) is the only mature React renderer of the three, so the viewport stays in the same component model as the 80% DOM UI. - WebGL2 now: WebGPU is in ≈86% of global browser usage (Chrome Android 100%, iOS Safari 77.3% — 10.23 of 13.23%, Chrome desktop 94.6%), WebGL2 at 99.5% of iOS Safari usage. Our scene (~20 low-poly ships, one light) is nowhere near WebGL2's limits, the WebGPU renderer costs +88 KB gzip, and
three/webgpucommits materials to TSL/NodeMaterial — a cost that only pays one way. - No wrapper: iOS 26 installs any site as a web app, push works after install via APNs, storage is 60% of disk and ITP-exempt once installed. Push without install, background work while closed and store presence are all optional for a check-in game.
Consequences
- 189 KB gzip renderer plus 253 KB for r3f + drei + three over a React-DOM baseline: bytes are a budget, not free.
- Mobile limits are fill rate, texture bandwidth and draw calls;
CONTEXT_LOST_WEBGLmust re-create the renderer anddevicePixelRatiois capped at 2 (§1.3, §3.2). - Notifications require installation on iOS and there is no background sync: offline actions are a queue replayed on next open, and nothing may depend on syncing while closed.
- The WebGPU seam stays a later, one-way-free move — revisit when GPU compute is wanted or iOS WebGPU crosses ~95% (§2.3).
Alternatives rejected
- Babylon.js 9.35.0. 694 KB gzip, +500 KB, for shadows and particles we do not need out of the box.
- PlayCanvas 2.24.0. 540 KB gzip and built around a hosted editor; our assets are procedural, from a Blender script.
- In-canvas UI. At 80% informational UI it means reimplementing text layout, momentum scrolling, selection, inputs, IME and accessibility, plus per-frame text rasterisation on a tile GPU (§3.1).
- Capacitor wrapper at launch. A second release pipeline (Xcode 26, APNs/FCM, store review) and a 15% WKWebView quota against 60% in Safari, for capabilities a check-in game does not need (§4, §5).
- WebGPU-first. +88 KB today, iOS at 77.3%, and the material commit is one-way: WebGL2 → WebGPU is free through the seam, the reverse is a custom-material rewrite (§2.3).