Perf tuning - #469
Conversation
Only await render results that can actually be promises. String and array results can continue through the async renderer without an extra microtask. Impact: small synchronous trees rendered through renderToStringAsync complete 16.7-19.8% faster across three 25-sample median runs (2,000 sequential renders per sample) versus main@7788814. Large-tree throughput is neutral.
Replace the regular-expression precheck with indexOf scans. Plain strings return before the character-by-character escaping loop, while strings containing entities retain the existing encoder. Impact: plain-text serialization is 18.4-21.3% faster across three 31-sample median runs (50 renders of 200 text nodes per sample) versus parent 986e7eb. Escaped-text performance is neutral (+0.4-1.3%).
Guard namespaced-attribute regex work by the required x prefix and compare the two enumerated HTML attributes directly instead of consulting a Set for every prop. Impact: HTML attribute-heavy serialization is 8.0-8.1% faster and SVG attribute-heavy serialization is 3.5-4.9% faster across three 31-sample median runs (50 renders of 250 elements per sample) versus parent 1fd90ec.
Read type.prototype once when distinguishing class components from function components, avoiding a duplicate property lookup for every rendered component. Impact: deep function-component trees are 6.1-6.8% faster, wide function-component trees are neutral to 2.1% faster, and class-component trees are 2.3-5.9% faster across three 31-sample median runs (40 renders of 500 components per sample) versus parent 012ffc6.
Use a character-code loop for strings shorter than eight characters and retain native indexOf scans for longer strings. Escaped short strings continue encoding from the first entity found by the fast-path scan. Impact: short safe strings are 7.7-15.8% faster and short escaped strings are 10.6-17.4% faster across three 31-sample median runs (50 renders of 400 text nodes and attributes per sample) versus parent df191a8. Long strings remain neutral.
Replace the namespace test-and-replace regex pair with a prefix-length and uppercase check for xml, xmlns, and xlink attributes. Non-x attributes retain the existing short-circuited path. Impact: namespace-heavy SVG serialization is 139.0-140.0% faster and coordinate-heavy SVG is 1.0-1.8% faster across three 31-sample median runs (50 renders of 500 elements per sample) versus parent 6ca9286. Ordinary HTML attributes remain neutral.
Handle a single numeric child alongside the existing direct string-child path instead of recursing through the full VNode serializer. Impact: numeric-child element lists are 9.1-14.2% faster across three 31-sample median runs (50 renders of 1,000 elements per sample) versus parent 6ef1ebe. Single-element, string, VNode, and array-child trees remain neutral.
Avoid running a regular expression for every JSX attribute by checking namespace prefixes and their uppercase boundary directly. Impact: 2.04x-2.22x namespace throughput for 300 SVG use elements across 31-sample runs. HTML, pretty, component, and shallow controls were neutral-to-faster. Tests: npm run test:vitest:run (202 passed)
Track the current suspension batch by length and remove it with one splice after it settles. This preserves active entries for repeated-suspension detection while avoiding filter/includes scans and recursive async calls. Impact: 1.4%-6.1% faster at 1,000 concurrent boundaries, 6.1%-6.5% at 2,500, and 10.3%-12.8% at 5,000 across 31-sample runs. Tests: npm run test:vitest:run (202 passed)
Only allocate Deferred and Promise.race when an AbortSignal is present. Readable stream and direct chunk renders otherwise await the suspension promise directly, while Node stream cancellation keeps the existing race. Impact: 1.8%-3.4% faster with one suspended boundary and 2.6%-3.2% with 10-100 across 31-sample runs. Abort-enabled controls were neutral. Tests: npm run test:vitest:run (202 passed)
|
| // Skip all work for strings with no entities needing encoding: | ||
| if (str.length === 0 || ENCODED_ENTITIES.test(str) === false) return str; | ||
| let i = 0; | ||
| if (str.length < 8) { |
There was a problem hiding this comment.
I tried all numbers and larger than 8 seemed to be slower in V8 😂
Use a character-code loop for strings shorter than eight characters and retain native indexOf scans for longer strings. Escaped short strings continue encoding from the first entity found by the fast-path scan.
Summary
This tunes several hot paths in synchronous, asynchronous, JSX, and streaming rendering:
renderToStringAsyncMeasured impact
renderToStringAsync: 16.7–19.8% faster