Repository navigation
perf(ranui): build an SSR element in 0.034µs instead of 1.389µs - #407
Merged
Merged
Conversation
`HTMLElementMock` held `style` and `classList` as instance fields containing
object literals of arrow functions. An arrow function closes over `this`, so it
cannot live on the prototype: every element allocated two objects and six
closures in its constructor, plus a Map for event listeners and another for
inline styles — whether or not anything ever touched them. Almost nothing does.
A generated page is overwhelmingly elements with a class attribute and a text
child.
They are created on first access now, and `inlineStyles` and `eventListeners`
on first write.
Measured, `new HTMLElementMock('div')` plus two attributes:
before 1.389 µs after 0.034 µs 41x
End to end, building and serialising a realistic 209-node sidebar 1,393 times
— the docs site's page count:
before 710 ms after 241 ms 2.9x
of which build 494 → 80 ms 6.2x
The bottleneck is now serialisation, which is the work that actually has to
happen. Output is byte-identical before and after, checked against the
equivalent string-template rendering.
How this was found is worth recording, because my first two explanations were
wrong. I said the cost was "five objects and two Maps per node"; a standalone
class of exactly that shape measures 0.060 µs, or 3.5% of it. Bisecting each
chained call showed `new HTMLElementMock()` alone accounted for 93%, with
`.class()`, `.attr()` and `.text()` adding a tenth of a microsecond between
them. Only then did reading the constructor make the closures obvious.
`inlineStyles` is now a `ReadonlyMap`: the empty map is shared by every element
that never set a style, so writing through it would leak one element's styles
into all of them. Nothing outside the mock writes to it, and the existing tests
read it with `.get()` and `.has()`, which the read-only type allows.
Nothing enumerates an element's own properties — the one thing that would have
noticed a field becoming a getter. 1,732 unit tests and 254 SSR tests pass
unchanged.
This is the SSR path only. In a browser the builder wraps real elements from
`document.createElement`, where `style` and `classList` are native; a component
costs ~15.6µs to construct there against 0.85µs for a plain element, which is a
separate question this does not touch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019pjSL6jGCw2wmBLhCAW35d
Deploying ran with
|
| Latest commit: |
dbcd7d3
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2159074c.ran-4ty.pages.dev |
| Branch Preview URL: | https://perf-builder-lazy-mock-field.ran-4ty.pages.dev |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
HTMLElementMockheldstyleandclassListas instance fields containing object literals of arrow functions. An arrow function closes overthis, so it cannot live on the prototype: every element allocated two objects and six closures in its constructor, plus a Map for event listeners and another for inline styles — whether or not anything ever touched them. Almost nothing does. A generated page is overwhelmingly elements with a class attribute and a text child.They are created on first access now;
inlineStylesandeventListenerson first write.Measured
new HTMLElementMock('div')plus two attributes:End to end — building and serialising a realistic 209-node sidebar 1,393 times, the docs site's page count:
The bottleneck is now serialisation, which is the work that actually has to happen. Output is byte-identical before and after, checked against the equivalent string-template rendering.
How it was found
Worth recording, because my first two explanations were wrong.
I said the cost was "five objects and two Maps per node". A standalone class of exactly that shape measures 0.060 µs — 3.5% of it. Bisecting each chained call showed
new HTMLElementMock()alone accounted for 93%, with.class(),.attr()and.text()adding a tenth of a microsecond between them. Only then did reading the constructor make the closures obvious.Safety
inlineStylesis now aReadonlyMap. The empty map is shared by every element that never set a style, so writing through it would leak one element's styles into all of them. Nothing outside the mock writes to it, and the existing tests read it with.get()and.has(), which the read-only type allows.utils/,components/,src/andtest/.Scope
This is the SSR path only. In a browser the builder wraps real elements from
document.createElement, wherestyleandclassListare native. A component costs ~15.6 µs to construct there against 0.85 µs for a plain element — a separate question this does not touch.🤖 Generated with Claude Code
https://claude.ai/code/session_019pjSL6jGCw2wmBLhCAW35d