DOM ENGINEV0.21.3FIZLET

03 DOCS · THE CONTRACT

Nine promises
Each one tested.

Most engines assume they own the page. Fizlet is a guest: it runs inside someone else's site, on a phone, next to their code, and it leaves no trace. These nine rules are the whole design. Every brick and every game built on Fizlet keeps them, and each one has a test that fails when it breaks.

The nine rules

  1. 01

    One clock per page

    Every engine on the page shares one requestAnimationFrame. A fixed 60 Hz step with a catch-up cap: a long frame drops time, it never spirals.

  2. 02

    Bounded everything

    Pools and the event ring have a fixed capacity and refuse work when full. Nothing grows during a session.

  3. 03

    Zero steady-state allocation

    One million kernel steps of a busy system cause no young-generation GC. Each brick alone allocates nothing.

  4. 04

    No layout reads in the step

    Read in read, simulate in step, write in render. @fzlt/core/debug counts any layout read made during a step.

  5. 05

    Change-only writes

    The ledger skips writes that change nothing. Writing the same value twice makes zero DOM mutations.

  6. 06

    Pause by default

    Hidden tab, page leaving, root off screen, typing in the host's own fields. Zero CPU while paused.

  7. 07

    Own nothing you can't give back

    Every page change goes through the ledger. destroy() restores it exactly, and a newer host edit always wins.

  8. 08

    Embed-safe

    No globals, no global styles, many engines per page. Every root Fizlet owns is CSS-contained. One AbortSignal removes every listener and observer.

  9. 09

    Budgets in CI

    Kernel at most 4 KB gzip; every brick and the page build have their own budget. Over budget, the build fails.

How each rule is checked

  • One clock. A kernel test runs several engines and counts one requestAnimationFrame between them; another checks the fixed step drops time after maxSteps (default 4) instead of spiralling. Details in the loop.
  • Bounded. Pools and the event ring (64 events a frame by default) refuse work at their cap, and the test checks they never grew. @fzlt/core/fx refuses particles past its cap (420 by default) and counts them in fx.dropped.
  • Zero allocation. One million kernel steps of a busy system cause 0 young-generation GCs on Node 24 (best of three windows; the test fails above 1). Bricks with a hot path have their own allocation tests.
  • No layout reads in the step. @fzlt/core/debug's guardLayout(engine) counts layout reads made during a step. The live demo on the home page shows the count: it stays at 0.
  • Change-only writes and Recall. Browser tests in Chromium write the same value twice and count zero mutations, then destroy() and compare the DOM with a snapshot taken before. See Recall.
  • Pause by default. A browser test hides the tab, types in a host field and scrolls the root away, and checks the engine pauses for each reason (engine.paused) and runs again after.
  • Embed-safe. A browser test compares every box inside the pad, the match overlay and the launch burst with CSS containment on and off: nothing moves or clips.
  • Budgets. npm run size builds every entry, minifies and gzips it, and fails CI over its budget. The numbers are in budgets.

Questions

Does a game have to keep the contract too?

Yes. The bricks keep it by construction, and the games on Fizlet are tested against it: they step on typed arrays, write through the ledger and give the page back on Escape.

Is any rule a frame-rate promise?

No. The rules bound the work an engine does; real frame time still depends on the page, the fonts and the device. What we measure is in the perf harness, under a 4× CPU throttle.

Can I break a rule on purpose?

A change that breaks one needs a test that proves the new rule. That is how the fps cap changed in 0.19.0: it now caps requestAnimationFrame, with a test that counts the calls a second at 60 and 120 Hz.