Performance

Cheap by design

One canvas, one requestAnimationFrame loop, ~130 particles at default density. The engine is built to stay off the main thread's way.

What runs each frame

  • The body-force loop — O(particles × bodies), range-culled: a body past its reach is skipped before any maths, on squared distance (no sqrt).
  • Integration + damping — a few flops per particle, plus a spatial-hash reindex for neighbour queries.
  • Render — plain additive arcs. The path uses zero shadowBlur (the most expensive canvas op); glow is a cheap halo under each dot.
  • No per-frame reflowscrollHeight is cached and variable-font writes are quantized, so the loop doesn't thrash layout.

Benchmark

The integrator hot path, measured across scales well beyond a real page:

Benchmark
scale     load              ms/frame    fps      throughput
light     800p ×  3b          0.25      ~3900     ~9.5M int/s
typical   2000p ×  6b         0.58      ~1730     ~21M  int/s
heavy     5000p × 10b         1.84       ~540     ~27M  int/s
stress    10000p × 16b        6.11       ~164     ~26M  int/s
# pnpm --filter field-ui bench  (Node 22, one core)

A real page runs ~130 particles against a handful of bodies — the "light" row and below — so the simulation is a rounding error; the render and layout discipline above are what keep it smooth.

It pauses itself

  • Backgrounded tab — the loop stops on visibilitychange and resumes on return.
  • Reduced motionprefers-reduced-motion freezes the sim (dt = 0) to a single static frame.
  • Field Cells<field-cell> demos gate their loop on an IntersectionObserver, so off-screen cells cost nothing.

Tuning

Lower density for very large or low-power surfaces; density: 0.5 halves the particle count. The field is one shared canvas, so the cost does not grow with the number of bodies on the page beyond the per-body loop term.