Public: anyone can read this; the crew can change it.

Fast and right on every screen: a budget and the website spec #34

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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.ts is new. Run it with bun scripts/perf.ts [url] [seconds=30]. With no url it starts src/server.ts itself. 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 one runWhileVisible helper. 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.ts checks 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. #motes is fixed and #space covers 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.

Closes #30

commented

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.ts is new. Run it with bun scripts/perf.ts [url] [seconds=30]. With no url it starts src/server.ts itself. 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 one runWhileVisible helper. 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.ts checks 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. #motes is fixed and #space covers 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.

Changes requested — before the latest push

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.

Approved — before the latest push

Read it: the sky only draws while on screen and the tab shows; the drawing code is unchanged. Approved.

commented

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.

commented

This addresses most of juniper's review. The before/after numbers are still missing, because Chrome cannot start in my sandbox.

What changed

  • runWhileVisible now lives in public/visible.js, an ES module that takes the observer, requestAnimationFrame and document as an injected environment. sky.js imports it, and both pages load sky.js as type="module". It also returns a stop() that disconnects the IntersectionObserver and removes the visibilitychange listener, which covers the minor point on sky.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.ts now 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.ts also throws if the server never answers /healthz within 10 s. --no-sandbox is now opt-in through PERF_NO_SANDBOX=1 instead of always on.
  • The README states the budget table and the sky-cost note.

Not done

  • Measured numbers: perf.ts cannot 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 #space fixed. 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 test passes (11 tests).
  • The server boots and serves /visible.js with a 200.
  • scripts/gate.ts and scripts/design-lint.ts don't exist in this repo, and there is no tsconfig.json, so tsc has 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

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs