Dispatch · The paper
Web optimization
Still vanilla at the core — static HTML, import-map Three.js, no bundler. The 1.8 MB file, GPU instancing, zero TBT, and Lighthouse 98 in the first draft were not. This note corrects that August pass and records what actually shipped afterward.
I wanted to keep the paper simple: static HTML, Three.js 0.169.0 on an import map. No React, no bundler. That constraint still holds. The performance numbers in the first version of this note did not — and neither does the front-page wiring from mid-August if you open the repo today.
The KiCad export is still enormous raw — about 31 MB and tens of thousands of pad meshes. What loads in the browser is a 1.26 MB Draco file that scripts/optimize-pcb.mjs wrote on 15 August from the KiCad 10.0.4 source tagged alu2.glb. This page is the correction.
| Before (raw KiCad in the tab) | After (what alu.glb is) |
|
|---|---|---|
| GLB | ~31 MB | 1,255,028 bytes (1.26 MB) |
| Meshes | ~41k pad/trace primitives | 9 meshes, 13 primitives, 13 materials |
| Compression | none | KHR_draco_mesh_compression |
| GPU instancing | no | no — join() by material |
| Copper vs mask | palette() welded both opaque | separate copper, pad, silk, mask, FR4 |
| Client merge | walked every mesh on the main thread | still in pcb-look.js; skips if meshes < 32 |
| Stack | import map, demand-render | same · pixel ratio cap 2 · preload alu.glb · landing shell |
The table above is the honest comparison. I had written 1.8 MB, claimed we ripped out the client-side merger, and implied GPU instancing. None of that matched the file in assets/pcb/alu.glb.
Shrinking the KiCad export
optimize-pcb.mjs does dedup → join(keepMeshes) → flatten → join → weld → prune → sparse → draco. It does not call palette() or simplify(). Palette was the previous pipeline’s bug: one atlas, then join(), copper gone under an opaque mask. Draco plus per-layer materials is how the file got small and still shows traces.
loadFittedPcb() in pcb-look.js is the one load path: Draco decode, dress materials, seat the board on y = 0. The bench and the full-screen viewer both call it. The merger is a guard for a raw 41k export, not a step we run on the file in the repo.
Look on the board: soldermask #1a9a48 at opacity 0.86, clearcoat 1, FR4 #0c3320, copper #c4a020. Hemisphere key is 0.6, not 2.5. Mask opacity is not 0.48 — that would have been a different (worse) board.
What Lighthouse actually reported
web/tests/lighthouse_desktop.json is a desktop run of index.html on localhost at 16 August 03:25Z. Chrome extensions were still on. Scores from that file:
| Category | Score |
|---|---|
| Performance | 43 |
| Accessibility | 100 |
| Best practices | 96 |
| SEO | 100 |
Same run: FCP 0.9 s, Speed Index 1.2 s, CLS 0.046, TBT 3,130 ms, LCP 10.8 s, TTI 10.8 s, max potential FID 3,530 ms. That is the ~3.5 second main-thread freeze from the first draft — measured with extensions on and the heavy KiCad spin still in the hero. It is nowhere near “TBT 0 ms, Performance 98.”
A later pass scored about 97 / 100 / 92 / 100 on the front and 100 / 98 / 96 / 100 on the viewer — hand-photo hero, lazy bench, print-media fonts. That pass is not the JSON checked into tests/, and it is not what the front page does now. Re-run incognito if you need a number you can cite; use npm run serve so cache and gzip match production.
Still vanilla, more deliberate
The stack did not become a framework. It became honest about what the hero costs.
index.html now inlines critical CSS for the bench shell, loads landing.css and site.css, and pulls magazine.css asynchronously so the broadsheet does not block the GLB. Three.js still comes from the import map; bench.js mounts the same way as on board.html, with preload on assets/pcb/alu.glb and modulepreload on the bench module. A fifteen-line landing.js is the only bootstrap left — it defers the compiler reels below the fold. Interior pages still use one magazine stylesheet and Google Fonts; the landing hero uses system UI until the magazine sheet arrives.
For a while I swapped the hero for a hand photo (pcb-arrive.webp, Fig. 1) because the KiCad orbit was eating first paint. That was the right August trade. The live bench is back in the hero now — the copper is the story again — and the hand shot lives in the gallery and journal where it belongs. The orbit MP4 (223 frames, ~5 MB) is still on the page for people who want the designed board to spin.
Local preview: cd web && npm run serve (scripts/serve.mjs) adds cache headers and gzip. Plain python3 -m http.server still works; it is just not what we audit against. npm run perf runs Lighthouse mobile and desktop on that server. Mobile performance on the current landing is high‑90s there — I am not pinning a hero score in the journal again.
What the viewer actually does
Full-screen viewer.html is still OrbitControls with 1:1 drag, a closed Catmull-Rom story rail, soldermask peel, and demand rAF. Touch the canvas and the tour stops so you can orbit freely. No extra camera library — we tried a film-style pass with letterbox and fog and took it back out.
Keeping the stack vanilla was still the right call. Pretending the site never gained layers — or inventing scores for the changelog — was not.