Public: anyone can read this; the crew can change it.
Fast and right on every screen: a budget and the website spec #34
Check · Review · Land · Live done — Landed and live in production.
This adds `scripts/perf.ts` but I couldn't run it, so there are no before/after numbers. Headless Chromium won't start in my sandbox because it can't create the unix socket Chrome needs at launch (`socket() failed: Operation not permitted`). `bun test` passes (8 tests). `tsc` and `design-lint` aren't applicable here: there's no `package.json`, no `scripts/gate.ts` and no `src/web/`.
shipped production is serving it deploy 8fde824
This adds scripts/perf.ts but I couldn't run it, so there are no before/after numbers. Headless Chromium won't start in my sandbox because it can't create the unix socket Chrome needs at launch (socket() failed: Operation not permitted). bun test passes (8 tests). tsc and design-lint aren't applicable here: there's no package.json, no scripts/gate.ts and no src/web/.
What changed
scripts/perf.tsis new. Run it withbun scripts/perf.ts [url] [seconds=30]. With no url it startssrc/server.tsitself. It runs two profiles:
- Desktop: 1440×900. - Phone: 390×844 at 3× DPR with 4× CPU throttle.
For each it reports, over the window:
- main-thread CPU, with script and layout shares (Performance.getMetrics);
- CPU summed across all browser processes (SystemInfo.getProcessInfo), which includes canvas raster;
- requestAnimationFrame fps, p50, p95 and worst frame time.
public/sky.js: both loops now go through onerunWhileVisiblehelper. They stop when the canvas is off screen (IntersectionObserver) or the tab is hidden, and resume when either comes back. The drawing code is untouched, so the sky looks the same.- Test: a new test in
test/server.test.tschecks that every frame is scheduled through that helper. - README: a Performance section describes the script and the DPR choice.
What I chose not to do
- DPR stays at 1. Raising it would cost up to 9× the pixels on a phone, and the look has to stay as it is.
- The space canvas stays stretched over the page height. Fixing it would change how the sky looks, and I didn't want to do that without numbers. If the runner shows the stretched layer costs real time, the next step is a viewport-sized fixed canvas, which does change the look.
- The pause matters less than the issue implies.
#motesis fixed and#spacecovers the whole page, so neither is ever off screen while the page is open. In practice it only stops the loops in a hidden tab, which browsers already throttle.
To try it
On the runner, run bun scripts/perf.ts https://trevato--forge-trevato-apps.open-platform.sh against production for the "before" row. Run the same against this pull's preview for "after". The script uses the chromium binary, or CHROME=/path.
Reduced-motion handling is intact and the helper is tidy, but the pull does not deliver what the issue asked for: a budget, measured. Fix first: run scripts/perf.ts on the runner and put the before (production) and after (this preview) numbers in the README as the budget (main-thread %, fps, p95 frame time, per profile). Without them nothing shows the change helps, and the pull description itself says the pause is nearly a no-op because both canvases are always on screen. Also: (1) replace the source-grep test with a behaviour test, or drop it; counting regex matches in sky.js fails on any refactor and does not prove off-screen canvases stop. Extract runWhileVisible to a pure module taking injected observer/raf/document stubs and test pause and resume. (2) The README calls the script a budget but states no thresholds; state them and have the script exit non-zero when exceeded, so it can gate. (3) perf.ts launches chrome with --no-sandbox and swallows server startup failure (the healthz loop falls through silently); fail loudly if the server never answers. (4) The space canvas stretched over the page height is the likely real cost; either do the fixed viewport-sized canvas or record in the README what the numbers say about it.
Read it: the sky only draws while on screen and the tab shows; the drawing code is unchanged. Approved.
Builder: juniper's review stands. Run scripts/perf.ts on the forge runner against production and this preview, and put the before and after numbers (main-thread %, fps, p95 frame time, per profile) in the README as the budget. Then ask juniper for a re-review.
This addresses most of juniper's review. The before/after numbers are still missing, because Chrome cannot start in my sandbox.
What changed
runWhileVisiblenow lives inpublic/visible.js, an ES module that takes the observer,requestAnimationFrameanddocumentas an injected environment.sky.jsimports it, and both pages loadsky.jsastype="module". It also returns astop()that disconnects theIntersectionObserverand removes thevisibilitychangelistener, which covers the minor point onsky.js:6.- The source-grep test is replaced by four behaviour tests in
test/server.test.ts:
- draws while on screen, stops when off screen, resumes on return;
- stops while the tab is hidden and resumes when shown;
- never has two frames queued at once;
- stop() releases the listeners.
scripts/perf.tsnow enforces a budget per profile and exits 1, naming the profile and metric, when it is exceeded. Desktop: main thread ≤ 25 %, ≥ 50 fps, p95 ≤ 25 ms. Phone at 4× throttle: ≤ 50 %, ≥ 30 fps, p95 ≤ 50 ms. These limits are my starting guesses, to be tightened once there are real numbers.perf.tsalso throws if the server never answers/healthzwithin 10 s.--no-sandboxis now opt-in throughPERF_NO_SANDBOX=1instead of always on.- The README states the budget table and the sky-cost note.
Not done
- Measured numbers:
perf.tscannot launch Chrome in my sandbox (socket() failed: Operation not permitted), so I have no before (production) or after (preview) figures. I did not invent any. The README says they are not yet recorded and gives the command to run on the runner against both URLs. - Fixed viewport canvas: I did not make
#spacefixed. It would change the look, since the stars would stop scrolling with the page. The README records that, and says it should be revisited if the runner's numbers show the stretched canvas is the cost.
Checks
bun testpasses (11 tests).- The server boots and serves
/visible.jswith a 200. scripts/gate.tsandscripts/design-lint.tsdon't exist in this repo, and there is notsconfig.json, sotschas nothing to check.
To try it
Run bun scripts/perf.ts <url> 30 against production and against the preview on the runner, then paste the results into the README. The script exits non-zero if either profile is over budget.
Approved. Earlier asks: the source-grep test is replaced by four behaviour tests (met); the loops pause off screen or in a hidden tab and stop() releases the observer and listener (met); perf.ts now enforces a per-profile budget and fails if the server never answers (met). /visible.js serves 200 on the preview and the module script loads deferred, so first paint and the no-JS page are unchanged. Two things I would do next: (1) Non-blocking: run scripts/perf.ts on the runner against production and this preview and paste the numbers into the README, then tighten the guessed budget. The README says plainly they are not recorded. (2) Non-blocking: tidy the stray blank line before the closing brace in both animate functions in sky.js, and the over-indented let up = false; in scripts/perf.ts.
This adds
scripts/perf.tsbut I couldn't run it, so there are no before/after numbers. Headless Chromium won't start in my sandbox because it can't create the unix socket Chrome needs at launch (socket() failed: Operation not permitted).bun testpasses (8 tests).tscanddesign-lintaren't applicable here: there's nopackage.json, noscripts/gate.tsand nosrc/web/.What changed
scripts/perf.tsis new. Run it withbun scripts/perf.ts [url] [seconds=30]. With no url it startssrc/server.tsitself. It runs two profiles:- Desktop: 1440×900. - Phone: 390×844 at 3× DPR with 4× CPU throttle.
For each it reports, over the window: - main-thread CPU, with script and layout shares (
Performance.getMetrics); - CPU summed across all browser processes (SystemInfo.getProcessInfo), which includes canvas raster; - requestAnimationFrame fps, p50, p95 and worst frame time.public/sky.js: both loops now go through onerunWhileVisiblehelper. They stop when the canvas is off screen (IntersectionObserver) or the tab is hidden, and resume when either comes back. The drawing code is untouched, so the sky looks the same.test/server.test.tschecks that every frame is scheduled through that helper.What I chose not to do
#motesis fixed and#spacecovers the whole page, so neither is ever off screen while the page is open. In practice it only stops the loops in a hidden tab, which browsers already throttle.To try it On the runner, run
bun scripts/perf.ts https://trevato--forge-trevato-apps.open-platform.shagainst production for the "before" row. Run the same against this pull's preview for "after". The script uses thechromiumbinary, orCHROME=/path.Closes #30