Skip to content

test(web,signals): tier-1 coverage for the headless-UI props chain - #3448

Merged
ryansolid merged 1 commit into
nextfrom
bench/polymorphic-chain
Sep 15, 2026
Merged

ryansolid merged 1 commit into
nextfrom
bench/polymorphic-chain

Conversation

@ryansolid

Copy link
Copy Markdown
Member

What

Tier-1 benches and tests for the shape every headless-UI library (Kobalte) renders per element: merge(defaults)omit(consumed) → compiler mergeProps(call-site statics) stacked per component layer, ending in a polymorphic dynamic(() => props.as) and one spread.

The existing utilities.bench.ts / spread-enumerate.bench.tsx / dynamic-tag.bench.tsx each measure one op in isolation, and each looks cheap. The chain is where the cost lives — every layer re-processes every key of the layer below — and nothing in the repo measured or pinned it, so it was invisible to CodSpeed.

Fixture

One shared source, packages/web/test/harness/polymorphic.tsx: Dialog.Trigger → Button.Root → Polymorphic mirroring the kobalte solid2 branch (context-driven aria, defaults, consumed keys, as="a" override shadowing the default), plus a CompiledRow floor twin that writes the same <a> directly. The DOM bench, SSR bench, parity scenario, and specs all render this identical tree, compiled by each project's generate.

Added

file measures / pins
bench web/test/polymorphic-chain.bench.tsx DOM: mount+clear 1k, update 10th ×16; owner-drift gated; chain vs floor
bench web/test/server/polymorphic-chain.bench.tsx SSR: renderToString 200 rows; chain vs floor (pure construction cost)
bench signals/tests/store/props-chain.bench.ts merge/omit chain at depth 1/3/7; build and build + consume separately
harness polymorphic-chain, polymorphic-chain-compiled-floor scenarios hydration ids through the chain, element identity across update, adopt-all, no separators
spec web/test/polymorphic-chain.spec.tsx, server/polymorphic-chain.spec.tsx chain resolves to the floor's exact attribute set; consumed keys and as absent; reactive attrs live; element kept across updates; SSR same set + keys
spec signals/tests/store/utilities.test.ts "props chain" shadowing order, hidden keys accumulate through nested omits and survive re-merges, live reads through every layer, spread-copy snapshot, descriptor kind (data vs getter) survives to the bottom of the chain

The last one is the contract the upcoming view work must keep: a consumer at the bottom of the chain (spread's children fast path, static-source detection) has to be able to read the compiler's static/dynamic verdict for a key.

Baseline (next, this machine)

lane compiled floor chain ratio
SSR, 200 rows 0.21 ms 3.87 ms 18× (≈19 µs/element)
DOM mount+clear 1k 20 ms 49 ms 2.5×
DOM update 100/1k ×16 2.8 ms 6.3 ms 2.3×
signals chain depth 3 21 µs
signals chain depth 7 65 µs

No source changes, no changeset. Both compilers verified (JSX_COMPILER=babel A/B on the specs and harness).

@changeset-bot

changeset-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 9f465d5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

Every element a component library like Kobalte renders is reached through
a stack of merge(defaults) → omit(consumed) → mergeProps(call-site statics)
layers ending in a polymorphic `dynamic(() => props.as)` and one spread.
Nothing in the repo measured or pinned that shape: the existing utilities
and spread benches exercise one merge, one omit, or one spread in
isolation, and each looks cheap. The chain is where the cost lives — every
layer re-processes every key of the layer below — and it was invisible to
CodSpeed.

One shared fixture (`test/harness/polymorphic.tsx`: Dialog.Trigger →
Button.Root → Polymorphic, plus a compiled floor twin rendering the same
`<a>`) feeds all of:

- `web/test/polymorphic-chain.bench.tsx` — DOM lane: mount+clear 1k rows,
  update 10th ×16, owner-drift gated. Chain vs compiled floor.
- `web/test/server/polymorphic-chain.bench.tsx` — SSR lane: renderToString
  of 200 rows, chain vs floor. Construction cost only; no update phase to
  amortize it.
- `signals/tests/store/props-chain.bench.ts` — the pure merge/omit chain at
  depth 1/3/7, build and build+consume reported separately so a change that
  moves cost between construction and read is visible.
- Parity-harness scenarios `polymorphic-chain` and
  `polymorphic-chain-compiled-floor`: hydration ids through the chain,
  element identity across an update, adopt-all, no separators.
- `web/test/polymorphic-chain.spec.tsx` / `server/polymorphic-chain.spec.tsx`:
  the chain resolves to the floor's exact attribute set (shadowing through
  the layers, consumed keys and `as` absent), reactive attrs stay live, the
  element is kept across updates; SSR serializes the same set with keys.
- `utilities.test.ts`: chain-shaped semantics for merge/omit — shadowing
  order, hidden keys accumulating through nested omits and surviving
  re-merges, live reads through every layer, spread-copy snapshot, and the
  descriptor kind (data vs getter) surviving to the bottom of the chain.

Baseline on next, per element: SSR chain ≈18× the compiled floor
(≈19 µs vs ≈1 µs), DOM mount ≈2.5×, DOM update ≈2.3×; signals chain
≈21 µs at depth 3, ≈65 µs at depth 7. These are the numbers the props
view work is measured against.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid force-pushed the bench/polymorphic-chain branch from aa666b4 to 9f465d5 Compare September 15, 2026 05:50
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 34934352482

Warning

No base build found for commit a5d8eae on next.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 71.842%

Details

  • Patch coverage: No coverable lines changed in this PR.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 1007
Covered Lines: 772
Line Coverage: 76.66%
Relevant Branches: 790
Covered Branches: 519
Branch Coverage: 65.7%
Branches in Coverage %: Yes
Coverage Strength: 15.1 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Sep 15, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 12.09%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 159 untouched benchmarks
🆕 12 new benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
projection derive: write one NESTED field (reference) 2.8 ms 2.5 ms +12.09%
🆕 build N/A 286.3 µs N/A
🆕 build + consume N/A 271.1 µs N/A
🆕 build N/A 509.1 µs N/A
🆕 build + consume N/A 486.4 µs N/A
🆕 build N/A 2.4 ms N/A
🆕 build + consume N/A 1.1 ms N/A
🆕 polymorphic-chain mount+clear 1000 rows: chain N/A 841.9 ms N/A
🆕 polymorphic-chain mount+clear 1000 rows: compiled N/A 380.9 ms N/A
🆕 polymorphic-chain update 100/1000 rows × 16: chain N/A 93.1 ms N/A
🆕 polymorphic-chain update 100/1000 rows × 16: compiled N/A 45 ms N/A
🆕 polymorphic-chain: 200 rows (renderToString): chain N/A 53.2 ms N/A
🆕 polymorphic-chain: 200 rows (renderToString): compiled N/A 5.7 ms N/A

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing bench/polymorphic-chain (9f465d5) with next (a5d8eae)

Open in CodSpeed

@ryansolid
ryansolid merged commit 08fe835 into next Sep 15, 2026
7 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.

2 participants