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
- 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.
- 02
Bounded everything
Pools and the event ring have a fixed capacity and refuse work when full. Nothing grows during a session.
- 03
Zero steady-state allocation
One million kernel steps of a busy system cause no young-generation GC. Each brick alone allocates nothing.
- 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.
- 05
Change-only writes
The ledger skips writes that change nothing. Writing the same value twice makes zero DOM mutations.
- 06
Pause by default
Hidden tab, page leaving, root off screen, typing in the host's own fields. Zero CPU while paused.
- 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.
- 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.
- 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
requestAnimationFramebetween them; another checks the fixed step drops time aftermaxSteps(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/fxrefuses particles past its cap (420 by default) and counts them infx.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'sguardLayout(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 sizebuilds 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.