Conversation
…nstructors (hoistProps)
A component's props literal with getters — the compiler's own `{ get x() {
return sig() }, ... }` — is built by V8 in dictionary mode with a closure
per getter per instance. With `hoistProps` (default on) both compilers emit
a module-level constructor whose getters live on the prototype and read
their captures through symbol slots on the instance; the call site becomes
`new _P$(a, b)`. Own keys, order, descriptors and prototype are unchanged,
so `Object.keys`, spread, `hasOwn`, `isStatic()` and `hasStaticKeys()`
answer as before.
The contract that changes: a props getter is defined only for a read
through its own object. Copying its descriptor onto another object
(`Object.defineProperty(target, k, getOwnPropertyDescriptor(props, k))`)
no longer works; in dev the getter throws naming the rule. Our own
primitives never copy descriptors except omit()'s no-Proxy fallback, fixed
in #3544 to re-home accessors.
Sites the pass leaves as literals: getters that close over a binding that
is reassigned or declared after the site, `this`/`super`/`arguments`, or a
module-level site (nothing to hoist above). Compiled template temps that
belong to a single getter move into it.
Babel (`src/ssr/props.ts`) and the native compiler (`src/ssr/props.rs`)
emit byte-identical output, including Babel's `_p`, `_p2`…`_p9`, `_p0`,
`_p1`, `_p10` parameter sequence; the native compiler additionally marks
the IIFE it generates for module-level `this` capture as transparent to
the depth rule, which Babel does not need.
Measured (yak-bench, SSR CPU per render, native compiler, mprim lane):
component-heavy pages +15–31%, geomean 1.12×; Octane ssr-throughput and
streaming-ssr flat (element-heavy fixtures, no getter literals on the hot
path).
Co-Authored-By: Claude via Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: 0f90eae The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
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 |
Coverage Report for CI Build 35401656264Coverage remained the same at 71.366%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will degrade performance by 9.95%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | polymorphic-chain: 200 rows (renderToString): chain |
20.7 ms | 23 ms | -9.95% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing perf/ssr-props-constructors (0f90eae) with next (49887e1)
Footnotes
-
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. ↩
What
With
hoistProps(default on) both compilers emit a component's props literal that has getters as a module-level constructor instead of an object literal:Own keys, order, descriptors and prototype are unchanged, so
Object.keys, spread,hasOwn,isStatic()andhasStaticKeys()answer as before. Sites that close over a reassigned or later-declared binding,this/super/arguments, or sit at module level keep the literal. Babel (src/ssr/props.ts) and the native compiler (src/ssr/props.rs) emit byte-identical output.Tracker: #3389 · design/issue: #3511
The contract that changes
A props getter is defined only for a read through its own object. Copying its descriptor onto another object (
Object.defineProperty(target, k, Object.getOwnPropertyDescriptor(props, k))) no longer works — the getter reads its capture through the receiver. In dev the getter throws naming the rule.omit()'s no-Proxy fallback, which signals: omit()'s copy path re-homes accessors with the source as receiver #3544 fixes to re-home accessors. Merge signals: omit()'s copy path re-homes accessors with the source as receiver #3544 first.combineProps/targetPropsdo copy descriptors; thesolid-mergedyak-bench lane crashes under this output (Cannot read properties of undefined (reading 'group')). Their fix is a one-line re-home incopyProps, or moving tomerge(). Heads-up to next-yak is timed with the release that flips this on.Measured
yak-bench, SSR in-process renderToString, instances/sec, native compiler,
mprimlane (Solid primitives),hoistPropson vs off, same run alternated. Round 1 is the clean pair; round 2 was disturbed mid-run (react control dropped 30% ontabs), so the two-round average is shown as the conservative figure.Octane
ssr-throughput(news-50/500) andstreaming-ssrSolid fixtures: flat on/off — element-heavy, no getter literals on the hot path — so no regression on the board.Against yak's own hand-rolled
mergedruntime (last clean rc.8 run, same machine, indicative):mprim+ hoisting now leads on every multi-component page (polymorphic-chain 1.49×, product-grid 1.42×, multifile-composition 1.27×, tabs 1.24×) and trails on the single-styled-element micro-cases (btn-variant / dyn-fair / dyn-translate 0.57×, realistic-button 0.86×).Merge gate
That last row is the honest cost of the contract: once descriptor copying is off the table, a library like yak has to come through
merge()/ssrElement, and today those trail their eager copy on the micro-cases. A profile ofbtn-variantputs the gap entirely in the runtime (props view ~40%,ssrElementmulti-source path ~23%, attribute-name escaping ~13%, server memo owner ~10%); none of it is the compiler. That runtime work is in progress withbtn-variantparity againstmergedas the exit criterion.hoistPropsshould not default on in a published rc before that lands; if it doesn't make the same rc, flip the default tofalseinconfig.ts/config.rsbefore release.Tests
packages/babel-plugin/test/ssr-props.spec.js— hoisting rules, capture rules, name generation, and execution of the emitted code (getter semantics, own-key parity, dev receiver error).pnpm testinpackages/babel-plugin(264) andpackages/compiler(5785) green.