perf(web): port mockup frame fixes (instant motion, toast, store memo) #1044

Closed
kayg wants to merge 4 commits from perf/web-mockup-frame-fixes into dev
Owner

Ports the four frame fixes found in the mockup on 11-09 (#1031 comment 88902, #1030) into the real app (apps/web + packages/ui). One commit per item (item 2 dropped after measuring); base dev 0878cb550.

1. Keyboard motion switch per element (data-instant)

  • packages/ui/src/styles/base.css: the rule :root[data-last-input="keyboard"] * { transition-duration: 0s } is gone. It restyled every element on each flip of the root attribute (a key after a click, a click after a key), in the frame of the next press. Its replacement answers one element at a time: [data-instant], [data-instant]::before, [data-instant]::after.
  • inputSource.svelte.ts: new instant() (true after a key, else undefined, so Svelte drops the attribute). input.last still drives script animations, and the root attributes stay for rules that name their own elements and for focus rings.
  • Carry data-instant={instant()}: Morph (capsule, halo, glass, clip), Pill, ContextMenu menus, Selector, SquishSwitch, and ToastCapsule (slot, inner).
  • apps/web/src/components/Shell.svelte: Shell already had its own scoped .instant switch (rules that name its panes, frame, bar and editor column), but only Focus set it. The places toggle (Ctrl+\) now sets it too, and it covers the resizers. These were the only two keys-no-motion failures once the global rule went (in both Chromium and WebKit).
  • e2e/keys-no-motion.spec.ts: every "a key moves nothing" test stays as it was. Two guards are added:
    • no stylesheet rule matches every element under data-last-input/data-input (*, *::before, > *);
    • flipping the root attribute changes the transition of no element except those that asked (data-instant) and an explicit list of rules that name their own elements ([data-contact], [data-side], .list .press, .shade).

2. Frosted backdrop layer: dropped

Ported, measured in real Safari with frost on, and dropped: no win in apps/web (see the evidence comment). The mockup keeps its GlobalLayer fix.

3. ToastCapsule: transform-only spring, size from the ResizeObserver entry

The glass sits at its final size and only scales (scale(sx, sy) from the same springs; the transform is removed at rest). Its size comes from entry.borderBoxSize, never offsetWidth/offsetHeight. A resize redraws the old size as a scale inside the observer callback (after layout, before paint), so no frame shows it already settled. That frame is why the old $effect re-measured. The dot is the capsule scaled to 12 px tall (uniform scale keeps the corners round), as in the mockup. apps/web/src/components/bar/Toast.svelte sets no glass size and needed nothing.

4. Store: one note change wakes only the lists it is in

  • NotesIndex keeps a member set per list (journal, pinned, each tag and each parent tag; eachKey(note, fn) walks them without allocating) and records the lists each change touched. An edit that keeps kind, pin and tags only records; nothing moves between sets.
  • notes.svelte.ts keeps one $state version per list and remembers each list's array until its own version moves:
    • store.notes is the same array until a note changes;
    • listNotes('tag:x'), pinned and the Journal read their member sets (O(m log m)), not a filter over every note;
    • a #tag reader does not re-run when a note outside it is edited.
  • The tag picker's counts (tagCounts) are the sets' sizes (O(tags)), no longer a walk over every note.
  • Sizes: one id per (note × list it is in). Big-O per change: O(tags of the note). Load of a Home: one eachKey walk per note, no allocation.
  • Unit test src/lib/notes-index.test.ts: one edit touches the same 4 lists in a Home of 200 and of 20,000 notes. Retag, pin, day-note and delete touch exactly what they should. After churn, the sets give the same Journal, Pinned, tag lists and counts as the old full scans.

Note: dev's store.notes was already cached per change tick (unlike the mockup's [...map.values()]). The real cost was the single global tick plus the per-list full filters, and that is what this item removes.

Decisions (made, not asked)

  • The keyboard spec keeps its old guarantees and only adds guards. No test was weakened or removed.
  • Backdrop: promoted only while a filter is set (no layer memory for the default unfrosted look).
  • Dot shape: uniform scale (mockup behaviour) rather than a 12×12 circle squashed by a non-uniform scale.

Evidence (real Safari ABAB, specs, unit tests, bundle gate) follows as comments.

🤖 Generated with Claude Code

Ports the four frame fixes found in the mockup on 11-09 (#1031 comment 88902, #1030) into the real app (`apps/web` + `packages/ui`). One commit per item (item 2 dropped after measuring); base `dev` `0878cb550`. ## 1. Keyboard motion switch per element (`data-instant`) - `packages/ui/src/styles/base.css`: the rule `:root[data-last-input="keyboard"] * { transition-duration: 0s }` is gone. It restyled **every element** on each flip of the root attribute (a key after a click, a click after a key), in the frame of the next press. Its replacement answers one element at a time: `[data-instant], [data-instant]::before, [data-instant]::after`. - `inputSource.svelte.ts`: new `instant()` (true after a key, else `undefined`, so Svelte drops the attribute). `input.last` still drives script animations, and the root attributes stay for rules that name their own elements and for focus rings. - Carry `data-instant={instant()}`: Morph (capsule, halo, glass, clip), Pill, ContextMenu menus, Selector, SquishSwitch, and ToastCapsule (slot, inner). - `apps/web/src/components/Shell.svelte`: Shell already had its own scoped `.instant` switch (rules that name its panes, frame, bar and editor column), but only Focus set it. The places toggle (`Ctrl+\`) now sets it too, and it covers the resizers. These were the only two `keys-no-motion` failures once the global rule went (in both Chromium and WebKit). - `e2e/keys-no-motion.spec.ts`: every "a key moves nothing" test stays as it was. Two guards are added: - no stylesheet rule matches every element under `data-last-input`/`data-input` (`*`, `*::before`, `> *`); - flipping the root attribute changes the transition of no element except those that asked (`data-instant`) and an explicit list of rules that name their own elements (`[data-contact], [data-side], .list .press, .shade`). ## 2. Frosted backdrop layer: dropped Ported, measured in real Safari with frost on, and dropped: no win in `apps/web` (see the evidence comment). The mockup keeps its GlobalLayer fix. ## 3. ToastCapsule: transform-only spring, size from the ResizeObserver entry The glass sits at its final size and only scales (`scale(sx, sy)` from the same springs; the transform is removed at rest). Its size comes from `entry.borderBoxSize`, never `offsetWidth/offsetHeight`. A resize redraws the old size as a scale inside the observer callback (after layout, before paint), so no frame shows it already settled. That frame is why the old `$effect` re-measured. The dot is the capsule scaled to 12 px tall (uniform scale keeps the corners round), as in the mockup. `apps/web/src/components/bar/Toast.svelte` sets no glass size and needed nothing. ## 4. Store: one note change wakes only the lists it is in - `NotesIndex` keeps a member set per list (`journal`, `pinned`, each tag and each parent tag; `eachKey(note, fn)` walks them without allocating) and records the lists each change touched. An edit that keeps kind, pin and tags only records; nothing moves between sets. - `notes.svelte.ts` keeps one `$state` version per list and remembers each list's array until **its own** version moves: - `store.notes` is the same array until a note changes; - `listNotes('tag:x')`, `pinned` and the Journal read their member sets (O(m log m)), not a filter over every note; - a `#tag` reader does not re-run when a note outside it is edited. - The tag picker's counts (`tagCounts`) are the sets' sizes (O(tags)), no longer a walk over every note. - Sizes: one id per (note × list it is in). Big-O per change: O(tags of the note). Load of a Home: one `eachKey` walk per note, no allocation. - Unit test `src/lib/notes-index.test.ts`: one edit touches the same 4 lists in a Home of 200 and of 20,000 notes. Retag, pin, day-note and delete touch exactly what they should. After churn, the sets give the same Journal, Pinned, tag lists and counts as the old full scans. Note: dev's `store.notes` was already cached per change tick (unlike the mockup's `[...map.values()]`). The real cost was the single global tick plus the per-list full filters, and that is what this item removes. ## Decisions (made, not asked) - The keyboard spec keeps its old guarantees and only adds guards. No test was weakened or removed. - Backdrop: promoted only while a filter is set (no layer memory for the default unfrosted look). - Dot shape: uniform scale (mockup behaviour) rather than a 12×12 circle squashed by a non-uniform scale. Evidence (real Safari ABAB, specs, unit tests, bundle gate) follows as comments. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The rule `:root[data-last-input=keyboard] *` restyled every element on each flip of the root
attribute (40-60 ms in real Safari, in the frame of the next press; #1031). base.css now answers
only `[data-instant]`; Morph, Pill, ContextMenu, Selector, SquishSwitch and ToastCapsule carry
`data-instant={instant()}`. Shell's own `.instant` switch (rules naming its panes, resizers, frame,
bar and editor column) is now set by the places toggle too, not only by Focus, and covers the
resizers. keys-no-motion keeps every "a key moves nothing" test and adds two guards: no universal
rule under the attribute, and a flip changes no element that did not ask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A full-screen blur() without a layer of its own is re-rendered by WebKit in the frame where any
glass opens above it (110-290 ms frames at 1440 in real Safari, #1031). translateZ(0) on the
frosted .backdrop only; unfrosted and drawn (art) backdrops stay unpromoted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The glass sits at its final size and only scales; no width/height on a backdrop-blurred box per
frame, and no offsetWidth/offsetHeight read when a toast opens or its words change (#1030). A
resize redraws the old size as a scale in the observer's frame, so nothing jumps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
perf(web): one note change wakes only the lists it is in
Some checks are pending
CI / server (pull_request) Waiting to run
CI / rust (pull_request) Waiting to run
CI / web (pull_request) Waiting to run
Markdown extraction contract / markdown-native-contract (pull_request) Waiting to run
Markdown extraction contract / markdown-editor-contract (pull_request) Waiting to run
a57596b5e6
The notes store had one signal for every list: a journal entry about one note re-ran every list
reader, and Pinned and each tag re-filtered every note (#1030). NotesIndex now keeps a member set
per list (journal, pinned, each tag and parent tag; `eachKey` walks them with no allocation) and
records the lists each change touched; an edit that keeps kind, pin and tags only records.
notes.svelte.ts keeps one version per list and remembers each list's array until its own version
moves, so store.notes is the same array until a note changes, a #tag reader ignores edits outside
it, and the Journal and the tag picker's counts come from the sets (no scan of every note).
Unit test: one edit touches the same 4 lists in a Home of 200 and of 20,000 notes; after churn
the sets give the same Journal, Pinned, tag lists and counts as the old full scans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kayg changed title from perf(web): port mockup frame fixes (instant motion, backdrop layer, toast, store memo) to perf(web): port mockup frame fixes (instant motion, toast, store memo) 2026-11-09 18:59:54 +00:00
Author
Owner

Evidence (head b4fca844a; measured arms below)

Item 2 (backdrop compositor layer) is DROPPED from this PR. In the real app it is not a win. With the account's frost on (checked in every run: .backdrop filter: blur(24px); the frosted class and transform appear only on the arm that has the commit), real Safari, 10 rounds each:

arm S1 key→click: frames >25 ms S1 median max S2 Esc→reopen: frames >25 ms S2 median max
C: item 1 only (a66799a04) 18 36 23 43
D: items 1+2 (adb4b989c) 32 61 26 61

Both comparisons point the wrong way. The VM was noisy, so this does not prove a regression, but the no-regression rule rules the change out. My guess at the cause: in apps/web the frosted .backdrop already has contain: strict and its own stacking, unlike the mockup's GlobalLayer; promoting it only adds a full-screen layer. The mockup fix stays in the mockup.

Real Safari 26.5.2, Mac VM (macos-01), ABAB, fresh page load each run. A = dev 0878cb550, B = PR (a57596b5e: the three items here plus item 2, with frost off, i.e. item 2 inactive)

Servers on dev-01 (same binary for every arm, built from dev's crates), reached over a loopback forwarder. Each Home has 2,000 notes in 20 tags. 1440×948 (S3 at 390×844). The idle baseline (same session, 900 ms) is max 19 ms in every arm.

scenario metric dev PR
S1 key (↓ in note) → click opens Style sheet (10 rounds) first 3 frames after the press, median / worst 21 / 55 (two spikes of 48, 55) 21 / 24
frames >25 ms over the 900 ms open (sum of 10) 57 48
S2 Esc closes sheet → click reopens (10 rounds) median max frame of the open 49 33
frames >25 ms (sum) 42 39
S3 toast appears at 390 (delete from Info, 5 rounds) median max frame, toast mount → +900 ms 91 74
frames ≥50 ms (sum) 13 12
S4 2,000-note list, All open, 8 outside edits × 5 rounds (notes.append) main-thread blocked ms per edit, median / p90 21 / 234 10 / 67
S4 #t3 list open (100 notes), edits to notes outside it median / p90 3 / 110 0 / 22
idle windows in S4 (same shape) median / p90 0 / 95 0 / 9

"Blocked" is a MessageChannel ping probe: it sums gaps over 2 ms. Edits reached the page: All showed the edited note on top 32/40 (dev) and 33/40 (PR). The rest is my check's prefix match, not missed edits.
Mac condition: free pages were low at times (8–57 k pages of 16 KB, no swap). That's under the 2 GB rule, and it explains the shared outlier rounds. Every arm ran interleaved in the same session.
Mac gotcha found: a long-running Safari on the VM could not open IndexedDB ("UnknownError: Unable to open database file on disk"), so the app stayed on the setup screen (claimDevice throws inside checkAuth().catch(()=>{})). Quitting Safari before each run fixes it.

Specs, dev-01, --workers=1, against dev 0878cb550 (same binary)

preload, lazy-ui, lazy-offline, glass, glass-roots, shortcuts, keys-no-motion, list-catch-up, list-chrome, list-fit, list-search, sidebar-edge, note-bars, note-bars-motion, note-pills, toast-bar, tags, sheet-pinned, journal-trash:

project dev PR (a57596b5e)
chromium-1440 95 passed, 49 skipped 97 passed, 49 skipped (+2 = new keys-no-motion guards)
chromium-390 111 passed, 33 skipped 111 passed, 35 skipped
webkit-1440 93 passed, 51 skipped 95 passed, 51 skipped
webkit-390 108 passed, 36 skipped 108 passed, 38 skipped

0 failures on either side. After the shell fix, keys-no-motion passes in both engines. Before it, Ctrl+\ and Ctrl+. moved Shell's grid, panes and resizers. The specs ran on a57596b5e; the head only drops the item-2 Backdrop CSS. backdrop-page (look-* projects) was not run.

Unit tests and checks (dev-01)

  • vitest run, web: the new notes-index.test.ts passes 5/5. The rest of the suite: 207 passed at the first run.
  • vitest run, ui: 96/96.
  • svelte-check --threshold error: 0 errors, same 17 warnings as dev.

Bundle gate (perf/web.sh --bundle-only --gate): first paint ≤ dev NOT met

dev 0878cb550 PR a57596b5e
first-screen JS, brotli 223.653 KB 224.230 KB (+0.577)
first-screen JS, raw 788.087 KB 789.109 KB (+1.02)
first-screen CSS, brotli 17.81 17.80

Dev itself is already over its chunk.index budget line (223.2). Dropping item 2 removes about 21 B raw. Per module (from source maps):

module raw bytes
notes-index +530
ToastCapsule +299
inputSource / Morph / Pill (data-instant) about +350
lists.ts −120

I already compacted the store (one keyed member map, no per-note allocation, the Journal and tag counts from the sets). What remains is the fixes' own code. The only levers left are lazy-loading other features' first-paint code:

  • trash.svelte.ts, RowActions, backdrop-art: each changes some first-screen behaviour;
  • soft-picture: baked synchronously on a phone's first screen;
  • Toasts: eager on purpose.

I did not do that in this PR. #1022 frees 1.6 KB of first paint precisely "so the ports have room". With it merged, this PR is under dev's current first paint.

## Evidence (head `b4fca844a`; measured arms below) **Item 2 (backdrop compositor layer) is DROPPED from this PR.** In the real app it is not a win. With the account's frost on (checked in every run: `.backdrop` `filter: blur(24px)`; the `frosted` class and transform appear only on the arm that has the commit), real Safari, 10 rounds each: | arm | S1 key→click: frames >25 ms | S1 median max | S2 Esc→reopen: frames >25 ms | S2 median max | |---|---|---|---|---| | C: item 1 only (`a66799a04`) | 18 | 36 | 23 | 43 | | D: items 1+2 (`adb4b989c`) | 32 | 61 | 26 | 61 | Both comparisons point the wrong way. The VM was noisy, so this does not prove a regression, but the no-regression rule rules the change out. My guess at the cause: in `apps/web` the frosted `.backdrop` already has `contain: strict` and its own stacking, unlike the mockup's GlobalLayer; promoting it only adds a full-screen layer. The mockup fix stays in the mockup. ### Real Safari 26.5.2, Mac VM (macos-01), ABAB, fresh page load each run. A = dev `0878cb550`, B = PR (`a57596b5e`: the three items here plus item 2, with frost off, i.e. item 2 inactive) Servers on dev-01 (same binary for every arm, built from dev's crates), reached over a loopback forwarder. Each Home has 2,000 notes in 20 tags. 1440×948 (S3 at 390×844). The idle baseline (same session, 900 ms) is max 19 ms in every arm. | scenario | metric | dev | PR | |---|---|---|---| | S1 key (↓ in note) → click opens Style sheet (10 rounds) | first 3 frames after the press, median / worst | 21 / **55** (two spikes of 48, 55) | 21 / **24** | | | frames >25 ms over the 900 ms open (sum of 10) | 57 | 48 | | S2 Esc closes sheet → click reopens (10 rounds) | median max frame of the open | 49 | 33 | | | frames >25 ms (sum) | 42 | 39 | | S3 toast appears at 390 (delete from Info, 5 rounds) | median max frame, toast mount → +900 ms | 91 | 74 | | | frames ≥50 ms (sum) | 13 | 12 | | S4 2,000-note list, All open, 8 outside edits × 5 rounds (`notes.append`) | main-thread blocked ms per edit, median / p90 | 21 / 234 | 10 / 67 | | S4 #t3 list open (100 notes), edits to notes outside it | median / p90 | 3 / 110 | 0 / 22 | | idle windows in S4 (same shape) | median / p90 | 0 / 95 | 0 / 9 | "Blocked" is a MessageChannel ping probe: it sums gaps over 2 ms. Edits reached the page: All showed the edited note on top 32/40 (dev) and 33/40 (PR). The rest is my check's prefix match, not missed edits. **Mac condition:** free pages were low at times (8–57 k pages of 16 KB, no swap). That's under the 2 GB rule, and it explains the shared outlier rounds. Every arm ran interleaved in the same session. **Mac gotcha found:** a long-running Safari on the VM could not open IndexedDB ("UnknownError: Unable to open database file on disk"), so the app stayed on the setup screen (`claimDevice` throws inside `checkAuth().catch(()=>{})`). Quitting Safari before each run fixes it. ### Specs, dev-01, `--workers=1`, against dev `0878cb550` (same binary) preload, lazy-ui, lazy-offline, glass, glass-roots, shortcuts, keys-no-motion, list-catch-up, list-chrome, list-fit, list-search, sidebar-edge, note-bars, note-bars-motion, note-pills, toast-bar, tags, sheet-pinned, journal-trash: | project | dev | PR (`a57596b5e`) | |---|---|---| | chromium-1440 | 95 passed, 49 skipped | 97 passed, 49 skipped (+2 = new keys-no-motion guards) | | chromium-390 | 111 passed, 33 skipped | 111 passed, 35 skipped | | webkit-1440 | 93 passed, 51 skipped | 95 passed, 51 skipped | | webkit-390 | 108 passed, 36 skipped | 108 passed, 38 skipped | 0 failures on either side. After the shell fix, keys-no-motion passes in both engines. Before it, `Ctrl+\` and `Ctrl+.` moved Shell's grid, panes and resizers. The specs ran on `a57596b5e`; the head only drops the item-2 Backdrop CSS. backdrop-page (look-* projects) was not run. ### Unit tests and checks (dev-01) - `vitest run`, web: the new `notes-index.test.ts` passes 5/5. The rest of the suite: 207 passed at the first run. - `vitest run`, ui: 96/96. - `svelte-check --threshold error`: 0 errors, same 17 warnings as dev. ### Bundle gate (`perf/web.sh --bundle-only --gate`): **first paint ≤ dev NOT met** | | dev `0878cb550` | PR `a57596b5e` | |---|---|---| | first-screen JS, brotli | 223.653 KB | 224.230 KB (**+0.577**) | | first-screen JS, raw | 788.087 KB | 789.109 KB (+1.02) | | first-screen CSS, brotli | 17.81 | 17.80 | Dev itself is already over its `chunk.index` budget line (223.2). Dropping item 2 removes about 21 B raw. Per module (from source maps): | module | raw bytes | |---|---| | notes-index | +530 | | ToastCapsule | +299 | | inputSource / Morph / Pill (data-instant) | about +350 | | lists.ts | −120 | I already compacted the store (one keyed member map, no per-note allocation, the Journal and tag counts from the sets). What remains is the fixes' own code. The only levers left are lazy-loading other features' first-paint code: - `trash.svelte.ts`, `RowActions`, `backdrop-art`: each changes some first-screen behaviour; - `soft-picture`: baked synchronously on a phone's first screen; - `Toasts`: eager on purpose. I did not do that in this PR. #1022 frees 1.6 KB of first paint precisely "so the ports have room". With it merged, this PR is under dev's current first paint.
Author
Owner

Rebased on dev 7c50123c3 (after #1022): head 79f755ecd

Trims inside this PR's own code

  • instant() is now a one-liner: input.last === "keyboard" || undefined.
  • list-search.ts: the membersOf cache was already unused on dev and is removed. The search corpus is keyed on listNotes(scope), which is now one memoized array per list, so a #tag search no longer rebuilds its corpus when a note outside the tag changes. That code lives in the lazy ListSearch chunk: −0.022 KB.
  • Looked at and kept:
    • Per-list set bookkeeping in first paint. The first screen reads a list (Journal, All or a #tag), and the Journal and tag lists come from the member sets. Loading that code with a lazy list module would mean either a list that fills in a moment late, or a one-off O(N) rebuild once the module arrives. Neither is a fair trade for about 0.5 KB raw.
    • One data-instant per component (Morph 4 → 1, ToastCapsule 2 → 1). This would move bytes from JS into scoped CSS rules naming the same parts, for roughly zero net.
    • Shell's three instant = assignments. They stay: a pointer toggle has to reset the switch.

Bundle gate (perf/web.sh --bundle-only, dev-01, same session):

dev 7c50123c3 PR 79f755ecd delta
first-screen JS, brotli 222.508 KB 222.607 KB +0.099
first-screen JS, gzip 261.006 261.503 +0.497
first-screen JS, raw 782.533 783.535 +1.002
first-screen CSS, brotli 17.807 17.809 +0.002
total JS, brotli 508.817 509.063 +0.246

The raw growth is the same as before (+1.0 KB). What fell from +0.58 to +0.10 is how it compresses next to #1022's new layout. For scale, chunks this PR does not touch (vim, code-rust, WhenField) move by ±0.04 KB between the two builds.

Budget lines, regenerated: every measured line is within its perf/budgets.json line for both builds; chunk.index is 222.607 against 222.71. The chunk lines still sum to the 525.01 total. Following the "index follows the measured size" convention changes no line, so budgets.json is untouched.

Cross-cutting specs on all four projects at this head: results follow (queued behind today's train on dev-01).

## Rebased on dev `7c50123c3` (after #1022): head `79f755ecd` **Trims inside this PR's own code** - `instant()` is now a one-liner: `input.last === "keyboard" || undefined`. - `list-search.ts`: the `membersOf` cache was already unused on dev and is removed. The search corpus is keyed on `listNotes(scope)`, which is now one memoized array per list, so a #tag search no longer rebuilds its corpus when a note outside the tag changes. That code lives in the lazy `ListSearch` chunk: −0.022 KB. - Looked at and kept: - **Per-list set bookkeeping in first paint.** The first screen reads a list (Journal, All or a #tag), and the Journal and tag lists come from the member sets. Loading that code with a lazy list module would mean either a list that fills in a moment late, or a one-off O(N) rebuild once the module arrives. Neither is a fair trade for about 0.5 KB raw. - **One `data-instant` per component** (Morph 4 → 1, ToastCapsule 2 → 1). This would move bytes from JS into scoped CSS rules naming the same parts, for roughly zero net. - **Shell's three `instant =` assignments.** They stay: a pointer toggle has to reset the switch. **Bundle gate** (`perf/web.sh --bundle-only`, dev-01, same session): | | dev `7c50123c3` | PR `79f755ecd` | delta | |---|---|---|---| | first-screen JS, brotli | 222.508 KB | 222.607 KB | **+0.099** | | first-screen JS, gzip | 261.006 | 261.503 | +0.497 | | first-screen JS, raw | 782.533 | 783.535 | +1.002 | | first-screen CSS, brotli | 17.807 | 17.809 | +0.002 | | total JS, brotli | 508.817 | 509.063 | +0.246 | The raw growth is the same as before (+1.0 KB). What fell from +0.58 to +0.10 is how it compresses next to #1022's new layout. For scale, chunks this PR does not touch (`vim`, `code-rust`, `WhenField`) move by ±0.04 KB between the two builds. **Budget lines, regenerated:** every measured line is within its `perf/budgets.json` line for both builds; `chunk.index` is 222.607 against 222.71. The chunk lines still sum to the 525.01 total. Following the "index follows the measured size" convention changes no line, so `budgets.json` is untouched. Cross-cutting specs on all four projects at this head: results follow (queued behind today's train on dev-01).
Author
Owner

Cross-cutting specs at head 79f755ecd (dev-01, server built from dev 7c50123c3's crates, --workers=1)

preload, lazy-ui, lazy-offline, glass, keys-no-motion:

project result
chromium-1440 31 passed, 4 skipped, 0 failed
chromium-390 24 passed, 11 skipped, 0 failed
webkit-1440 30 passed, 5 skipped, 0 failed
webkit-390 23 passed, 12 skipped, 0 failed

No Mac turn: since the last real-Safari run, the code changes are instant() returning the same values in one line and the lazy ListSearch corpus key. Neither changes what runs in a frame.

## Cross-cutting specs at head `79f755ecd` (dev-01, server built from dev `7c50123c3`'s crates, `--workers=1`) preload, lazy-ui, lazy-offline, glass, keys-no-motion: | project | result | |---|---| | chromium-1440 | 31 passed, 4 skipped, 0 failed | | chromium-390 | 24 passed, 11 skipped, 0 failed | | webkit-1440 | 30 passed, 5 skipped, 0 failed | | webkit-390 | 23 passed, 12 skipped, 0 failed | No Mac turn: since the last real-Safari run, the code changes are `instant()` returning the same values in one line and the lazy ListSearch corpus key. Neither changes what runs in a frame.
Author
Owner

kayg OK'd the +0.1 KB first paint (11-10). Merged into dev; closing.

kayg OK'd the +0.1 KB first paint (11-10). Merged into dev; closing.
kayg closed this pull request 2026-11-10 05:20:42 +00:00
Some checks are pending
CI / server (pull_request) Waiting to run
CI / rust (pull_request) Waiting to run
CI / web (pull_request) Waiting to run
Markdown extraction contract / markdown-native-contract (pull_request) Waiting to run
Markdown extraction contract / markdown-editor-contract (pull_request) Waiting to run

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal!1044
No description provided.