Skip to content

perf(compilers): emit getter keys as string literals, never computed - #3514

Merged
ryansolid merged 1 commit into
nextfrom
perf/compiler-getter-keys
Sep 17, 2026
Merged

ryansolid merged 1 commit into
nextfrom
perf/compiler-getter-keys

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Finding 1 of #3511. Both compilers emit non-identifier getter keys in compiled props literals as computed accessors:

// today
Polymorphic(merge({
  get as() { return merged.as ?? "button"; },
  get ["aria-disabled"]() { return merged.disabled || void 0; },
  get ["data-loading"]() { return merged.loading ? "" : void 0; },}, others));

Babel does it via t.objectMethod("get", id, [], body, !t.isValidIdentifier(key)), and packages/compiler/src/shared/ast.rs mirrors it on purpose ("Babel: … non-identifier getter keys are computed"). get "aria-disabled"() is the same property, but a computed key in an object literal takes V8 off the literal boilerplate path into per-property runtime definition for the whole object.

Measured on ButtonRoot's seven-getter <Polymorphic> props literal from yak-bench's polymorphic-chain, build + read all 7 keys, min of 7 × 200k:

form TurboFan --no-opt
literal, computed keys (today) 966 ns 715 ns
literal, string-literal keys (this PR) 525 ns 485 ns
literal, all identifier keys (control) 482 ns 482 ns

So the string-literal form is within 10% of what the same object costs with plain identifier keys — the computed key was the cost, not the hyphen. Every aria-*, data-*, and class prop paid it (class is a reserved word, so isValidIdentifier rejects it too).

Change

id at every site is already an Identifier or a StringLiteral (from convertJSXIdentifier, or t.identifier for locked DOM properties), so computed is just false. Six Babel sites — shared/component.ts ×3, dom/element.ts, ssr/element.ts, universal/element.ts — and the single Oxc funnel object_getter_property_with_statements. Nothing else in the emitted code changes.

Fixtures regenerated in both trees (vitest -u for Babel, UPDATE_OXC_FIXTURES=1 UPDATE_PARITY=1 for Oxc, including the three cross-mode parity .diff files). Audited: every changed line is a getter key. Babel's generator prints a reserved-word string key bare (get class()), which is valid and the same property.

Verified

  • @solidjs/babel-plugin 242/242; @solidjs/compiler vitest 5785/5785 and cargo test across all three feature sets.
  • @solidjs/web DOM (825), hydrate (185), server (1001) suites run end-to-end through the rebuilt native compiler; harness artifacts unchanged.

Case-level SSR A/B in the yak-bench harness is pending a quiet machine — tonight's runs had a load average of 11 from a remote-desktop session and the raw numbers disagreed by 10× within a tag, so I'm not quoting them. The microbenchmark is deterministic and the emitted code is otherwise identical; I'll add the harness number when I have one worth trusting.

@changeset-bot

changeset-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 246eeeb

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

This PR includes changesets to release 11 packages
Name Type
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
test-integration Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/universal Patch
@solidjs/web 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

@coveralls

coveralls commented Sep 17, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 35200204109

Coverage remained the same at 71.46%

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: 1018
Covered Lines: 772
Line Coverage: 75.83%
Relevant Branches: 790
Covered Branches: 520
Branch Coverage: 65.82%
Branches in Coverage %: Yes
Coverage Strength: 14.93 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Sep 17, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 172 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing perf/compiler-getter-keys (246eeeb) with next (f0ee8a0)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Both compilers emitted non-identifier getter keys in compiled props
literals as computed accessors — `get ["aria-disabled"]() {}` — Babel via
`t.objectMethod("get", id, [], body, !t.isValidIdentifier(key))` and the
Oxc compiler mirroring it deliberately. `get "aria-disabled"() {}` is the
same property, but a computed key in an object literal drops V8 off the
literal boilerplate path into per-property runtime definition for the whole
object. Measured on ButtonRoot's seven-getter `<Polymorphic>` props literal
from yak-bench's polymorphic-chain: 966 ns → 525 ns per object to build and
read once (#3511, finding 1). Every `aria-*`, `data-*`, and `class` prop
paid it.

`id` at every site is already an Identifier or a StringLiteral (from
convertJSXIdentifier or `t.identifier`), so `computed` is simply `false`.
Six Babel sites (shared/component.ts ×3, dom/ssr/universal element.ts) and
the single Oxc funnel (`object_getter_property_with_statements`). Fixtures
regenerated in both test trees plus the three cross-mode parity diffs; every
changed line is a getter key. Babel prints reserved-word string keys bare
(`get class()`), which is valid and the same property.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
@ryansolid

Copy link
Copy Markdown
Member Author

Harness measurement, second attempt. The machine is still not quiet (load average 17–19: Spotlight indexing the new worktrees plus a remote-desktop session), and both wall-clock and process.cpuUsage per render swing ±40% within a single tag, so no time-based number is quotable yet.

What is stable under load is allocation. Scavenge count over a fixed render batch (--max-semi-space-size=1, --trace-gc enabled at runtime after warm-up; two reps each, reps agree to ±2%):

case baseline this PR Δ
polymorphic-chain (n=400 × 100) 807 / 770 733 / 734 −7%
tabs (150 × 100) 874 / 865 862 / 854 −1.5%
multifile-composition (150 × 60) 507 / 505 514 / 513 +1.5% (noise)
btn-variant (1000 × 150) 459 / 461 460 / 461 0

Consistent with the key mix: polymorphic-chain is the case whose props are mostly aria-* / data-*; the others are mostly identifier keys and never took the computed path. Allocation is a lower bound here, not the effect size — in the microbenchmark the computed form's extra cost is mostly the per-property runtime definition, and only ~14% of it is bytes. Time-based harness numbers still to come when the machine is quiet.

Claude via Cursor

@ryansolid
ryansolid force-pushed the perf/compiler-getter-keys branch from 8a0c039 to 246eeeb Compare September 17, 2026 08:32
@ryansolid
ryansolid merged commit 1e7ebe9 into next Sep 17, 2026
7 checks passed
@ryansolid

Copy link
Copy Markdown
Member Author

Harness SSR A/B on a quiet machine (load 5.5, Spotlight and the remote session gone). Baseline next at #3509 vs this branch, solid-mprim lane, both compiled with the respective checkout's Babel plugin; 8 interleaved pairs per case, fresh process each, 1 s warm + 2 s measure, instances/sec:

case median vs median paired-ratio median pairs favoring PR
polymorphic-chain (400) +7.5% +8.3% 6 / 8
tabs (150) +2.6% +6.4% 6 / 8
multifile-composition (150) +2.2% +2.8% 4 / 8 — noise-level
btn-variant (1000) −0.9% +0.2% flat

Baseline spread within a tag is ±3% on polymorphic-chain, ±12% on tabs, so the first row is a real effect and the second is suggestive. Same shape as the allocation proxy above: the win lands where the props are aria-* / data-* / class, and there is nothing to gain where every key is an identifier. polymorphic-chain SSR moves from 0.29× React to roughly 0.31×; the rest of that gap is finding 2 in #3511.

Claude via Cursor

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