Skip to content

perf(frames): scan for binding-slot attributes once, after the bind tier installs - #4058

Merged
ryansolid merged 4 commits into
nextfrom
claude/project-thread-271xwi
Oct 14, 2026
Merged

ryansolid merged 4 commits into
nextfrom
claude/project-thread-271xwi

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Before: on a page that announces the bind tier, every frame's slot walk visited every element looking for _s:* attributes while the tier was still loading. That work was thrown away, and after the tier installed the flush walked the page again and parsed the attributes. On the HN story page with binding slots, that came to 20,750 elements visited and 15,308 attribute reads.

After: while the tier is announced or loading but not yet resident, the walk visits comments only and marks the frame held. The install's flush does the one element scan. On the same page that is 10,375 elements and 7,658 attribute reads, and hasSlotMarker (~36 ms at 4x CPU) drops out of the profile.

Behavior note: once a page announces the bind tier, a frame with no _s:* attributes now also waits for that tier before it resolves. The tier is already loading because of the announcement, so the wait is bounded by that load. Pages that don't announce the tier, and pages where the tier is already resident, behave as before. Client slots (regions tier only) never ran the element scan, so they aren't affected.

How: before the walk, collectSlots checks for the held case (bind && !B && (tierLoads.bind || announced)). In that case it sets elements.b and turns bind off, so the walk loop itself is unchanged from next. A new test in tier-bind-announced-gate.spec.tsx counts element-visiting TreeWalkers: none while the frame is held, exactly one after release. The test fails without the fix.

Size: frames eager consumer goes 11,495 -> 11,509 B brotli (+14), and 34,411 -> 34,449 B minified. The cap is raised to that measurement and is pending the maintainer's OK. This PR also updates the #4042 note to say that cap was accepted on merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LZkrATgvFJjgFRdDB4Drcq

…ier installs

While the bind tier was announced or loading but not resident, every frame's
slot walk visited every element to check for `_s:*` attributes, then the
tier install's flush walked again and parsed them. The held walk now visits
comments only and marks the frame held; the flush does the single scan.

HN story page with binding slots: 20,750 -> 10,375 elements visited,
15,308 -> 7,658 attribute reads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZkrATgvFJjgFRdDB4Drcq
@changeset-bot

changeset-bot Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b111d5a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
test-integration Patch
board-example Patch
todos-server-example Patch
@solidjs/compiler Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/universal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.47 KB 0 B 0 B −8 B 7.48 KB ✅
signals: + createStore 14.94 KB 0 B 0 B −22 B 14.94 KB ⚠️ over by 3 B, 42 B minified headroom
signals: + isPending/latest 9.75 KB 0 B 0 B −8 B 9.77 KB ✅
app: render + one signal (the simple-app floor) 9.95 KB 0 B 0 B −8 B 9.98 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.89 KB 0 B 0 B −65 B 17.93 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.54 KB 0 B 0 B −83 B 29.57 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.04 KB 0 B 0 B −22 B 13.07 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.64 KB 0 B 0 B −22 B 14.64 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.23 KB 0 B 0 B +14 B 29.24 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.16 KB 0 B 0 B −8 B 10.17 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.80 KB 0 B 0 B −99 B 25.79 KB ⚠️ over by 12 B, 119 B minified headroom stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.69 KB 0 B 0 B −41 B 31.64 KB ⚠️ over by 54 B, 61 B minified headroom stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.54 KB +18 B (+0.2%) +34 B 0 B 11.54 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 34.27 KB +80 B (+0.2%) +34 B −1335 B 34.33 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 9.01 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 37.99 KB +61 B (+0.2%) +34 B −1404 B 38.15 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 9.02 KB, wire.js 0.93 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.08 KB +45 B (+0.1%) +34 B −2425 B 35.66 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, regions.js 0.80 KB, sc-comments.js 0.20 KB, trace.js 8.99 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.73 KB +20 B (+0.0%) +34 B −2409 B 41.21 KB ✅ eager (counted): web.js 21.56 KB; assets.js 0.78 KB, bind.js 1.84 KB, decode.js 6.23 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.97 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.55 KB −60 B (−0.1%) +34 B −1335 B 41.79 KB ✅ assets.js 0.78 KB, bind.js 1.85 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, server.js 1.02 KB, serverForms.js 3.58 KB, trace.js 9.01 KB, wire.js 0.94 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.35 KB +30 B (+0.1%) +34 B −1325 B 47.49 KB ✅ eager (counted): client.js 27.75 KB; assets.js 0.78 KB, bind.js 1.84 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.81 KB, server.js 1.02 KB, serverForms.js 3.30 KB, trace.js 8.97 KB, wire.js 0.94 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.57 KB 0 B 0 B 0 B 20.58 KB ✅

⚠️ Over the brotli cap within the minified allowance (passes)

  • signals: + createStore: over brotli cap by 3 B; minified 45,343 B vs 45,365 B recorded with the cap (−22 B) — 42 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store): over brotli cap by 12 B; minified 80,624 B vs 80,723 B recorded with the cap (−99 B) — 119 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 54 B; minified 100,920 B vs 100,961 B recorded with the cap (−41 B) — 61 B of the 20 B minified allowance left; +0 B minified over this PR's base

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

@coveralls

coveralls commented Oct 14, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 38593898440

Coverage remained the same at 77.515%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1326
Covered Lines: 1084
Line Coverage: 81.75%
Relevant Branches: 1040
Covered Branches: 750
Branch Coverage: 72.12%
Branches in Coverage %: Yes
Coverage Strength: 27.28 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 2 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 195 untouched benchmarks


Comparing claude/project-thread-271xwi (b111d5a) with next (45691d1)

Open in CodSpeed

ryansolid commented Oct 14, 2026 •

Copy link
Copy Markdown
Member Author

CodSpeed flags frames adoption walk › announced, no bind at 50.9 → ~55 ms (−7%). I don't think this PR causes it:

  • That cell's page announces ["regions"] only, so bind is false and the held-case check never runs.
  • Since 91264e1, the walk loop is byte-identical to next's. The PR only adds one guarded line before the walk. The flag is unchanged at −7.45%.
  • A local A/B of next's frame-client.ts against this head, three runs each, gives means of 7.56 / 7.82 / 8.20 ms on base and 7.74 / 8.01 / 8.38 ms on head, so there's no difference beyond noise.
  • CodSpeed itself warns "Different runtime environments detected" for this comparison. The base number comes from an earlier next run.

The benchmark job was re-run once and still flagged. This needs acknowledging on CodSpeed, or a fresh base run on next.


Generated by Claude Code

Fold the held case into `bind` before the walk instead of adding a separate
`scan` flag, so the walk loop compiles as on `next` for every page that
doesn't scan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZkrATgvFJjgFRdDB4Drcq
…alk as on next

The held case now passes no element map to `collectSlots`, so the walk
itself is byte-identical to `next`; `found.b` is noted before the walk.
CodSpeed flagged the announced-without-bind adoption walk at -7% with the
check inside `collectSlots`, though its path was unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZkrATgvFJjgFRdDB4Drcq
Re-measures the frames eager cap on top of #4055: 11,540 B brotli /
34,499 B minified.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KRieFbP8sapvTBygyVm3mg
@ryansolid
ryansolid merged commit f286a40 into next Oct 14, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants