SDEAI/MLLLM
Fit a WebGPU particle world into 304 KB, loaded after first paint
This portfolio, built as a production project with budgets, tests and CI
Sole engineer and designer
The problem
A portfolio that shows graphics skill usually costs load time and accessibility. This one had to be striking and still pass a strict engineering review.
Team: Just me. Timeline: September–October 2026.
What changed
- Graphics engine
- 304.6 KB
- Baseline: 320 KB budget
- Gzipped JavaScript the engine adds after first paint, measured on the built site by scripts/budgets.ts in CI
- JavaScript before idle
- 10.9 KB
- Baseline: 50 KB budget
- Gzipped initial JavaScript on project pages (40.3 KB on Home, which adds the one-time intro), same script
- GPU kernel against CPU spec
- < 0.2% error
- Playwright steps the WebGPU and WebGL2 kernels once and compares the buffers with the CPU simulation, in CI on Chrome 153
- Lighthouse performance (mobile)
- 99–100
- Three Lighthouse CI runs per page (Home, projects, a deep dive, a presentation, the Lab and the résumé) on the built site. CI has no GPU, so this measures the poster path; frame rates on real devices are still to be measured
Architecture
Request flows
- Full world: Browser → Static pages (Astro) → Boot code (poster check) → World engine → Compute kernel (WebGPU or WebGL2)
- Poster: Browser → Static pages (Astro) → Boot code (poster check) → Poster stills
- Open the explorer: Browser → Static pages (Astro) → Lab demos and explorer → World engine → Compute kernel (WebGPU or WebGL2)
Decision 1 of 3
How do GPU and CPU simulations stay in step?
- Options
- Write them separately and eyeball it · Generate both from one source · Treat the CPU version as the specification and test the GPU against it
- Chose
- The CPU version is the specification, and a test compares GPU read-back with it
- Trade-off
- The same logic exists twice, so every change touches both files.
- Evidence
- Changing one constant in the GPU kernel only makes the test fail with a 7.7% error against a 0.2% tolerance.
Decision 2 of 3
What happens on a browser without a usable GPU?
- Options
- Run the low tier anyway · Show nothing · Show a poster chosen before downloading the engine
- Chose
- A poster, chosen by about 1 KB of boot code
- Trade-off
- Visitors with a software renderer never see the moving world.
- Evidence
- Headless Chrome with software rendering blocked the main thread for 2.4 s per frame; with the check, blocking time is 0 ms.
Decision 3 of 3
How does a Three.js bug stay out of the kernel?
- Options
- Pin an older version · Work around it per call site · Write the kernel with no branches around buffer reads
- Chose
- No branches around buffer reads
- Trade-off
- The kernel evaluates all five formations for every particle.
- Evidence
- A 40-line reproduction shows WebGL2 reading wrong elements inside a second sibling If() block; the branch-free kernel matches the CPU specification on both backends. The reproduction is in docs/upstream/.
Blending formations without branching
const base = i.mul(STRIDE);
for (let k = 0; k < K; k++) {
const w = U.weights[k];
const A = formBuf.element(base.add(k * 2)).toVar();
const B = formBuf.element(base.add(k * 2 + 1)).toVar();
const f = formationTarget(k, A, B, h, U);
tgt.addAssign(f.pos.mul(w));
sig.addAssign(f.sig.mul(w));
vis.addAssign(f.vis.mul(w));
hue.addAssign(f.hue.mul(w));
rigid.addAssign(f.rigid.mul(w));
}Every formation is evaluated and weighted, so moving between shapes is one continuous blend, GPU threads never diverge, and the WebGL2 bug has nothing to trigger on.