Repository navigation
perf(react): serialize nested Stencil SSR children shallowly - #851
Conversation
Nested Stencil SSR components were fully rendered inside every ancestor's light DOM pass and again as real children, so server time doubled with each nesting level. Mark the SSR wrapper and, when resolving children, emit a bare tag with the same attributes instead of running the child. Each component now renders once and the output is unchanged. Fixes stenciljs#849
f5213b9 to
64b39a7
Compare
|
Thanks for this, the shallow-child path also addresses the nested-content failure in #852. I tested Draft #853 includes regression tests and a proposed correction for both cases. I think the relevant changes could fit into this work. Would you prefer them here or as a follow-up? |
|
Thanks for testing this and for the detailed repro, @wessmeister. I ran your two cases against
So both gaps are real, and you're right that this PR doesn't close them. It moves scoped nesting from "nothing rendered" to "rendered, minus the separator and object props". I'd like to keep this one small. It targets the perf problem in #849 and leaves declarative-shadow-DOM output unchanged, which should keep it easy to review. Your changes go further (compact serialization, the props channel), and I think they deserve their own review as a follow-up on top of this one. Happy to help with the rebase or with review when it gets there. I left a few notes on #853 from testing it. |
wessmeister
left a comment
There was a problem hiding this comment.
Thanks for checking those cases, @Burzmalian. This looks good to me as a focused performance fix. I'll keep the correctness follow-up in #853 and rebase once this lands.
OS-jacobbell
left a comment
There was a problem hiding this comment.
Thanks for opening the PR, looks good to me!
With `serializeShadowRoot: 'scoped'`, a parent's server HTML is its Stencil render, which also renders its nested Stencil children. Three things were lost there: - Whitespace next to nested children. Stencil's `prettyHtml` output was cut by lines and its formatting stripped again, together with real text, so `<Child /> text` rendered as `<child></child>text`. - Complex props of nested children. The shallow children from stenciljs#851 dropped them, so they rendered with their default values. - The light DOM of a parent whose nested child had a string `style`, which React rejects. Render without `prettyHtml` and cut the shadow root and light DOM out of the output by parser offsets, so whitespace stays as React rendered it. Keep the complex props of the host and its nested children per render, and assign them in `beforeHydrate` to the element with their `data-stencil-react-props` marker. Convert a string `style` of a nested child to the object React expects. Without `prettyHtml`, Stencil no longer sorts attributes and classes, so the order in the server HTML and in the snapshots changes. Fixes stenciljs#852
With `serializeShadowRoot: 'scoped'`, a parent's server HTML is its Stencil render, which also renders its nested Stencil children. Three things were lost there: - Whitespace in the light DOM. Stencil formatted its output with `prettyHtml`, and the adapter cut it by lines and stripped the formatting again, together with real text: `<Child /> text` rendered as `<child></child>text`, and `<pre>` content lost its line breaks. - Complex props of nested children. The shallow children from stenciljs#851 dropped them, so they rendered with their default values. - The light DOM of a parent whose nested child had a string `style`, which React rejects. Render without `prettyHtml` and cut the shadow root and light DOM out of Stencil's output by parser offsets, so the adapter no longer strips whitespace. Keep the complex props of the host and its nested children per render, and assign them in `beforeHydrate` to the element with their `data-stencil-react-props` marker, which is then removed. Markers that `beforeHydrate` can't reach, such as those in `<template>` or `<noscript>` content, are removed from the output. Convert a string `style` of a nested child to the object React expects. Without `prettyHtml`, Stencil no longer sorts attributes and classes, and declarative shadow roots no longer carry formatting whitespace in their text nodes. Fixes stenciljs#852
Pull request checklist
npm run build) was run locally for affected output targetsnpm test) were run locally and passednpm run prettier) was run locally and passedPull request type
What is the current behavior?
createComponentForServerSideRenderingserializes each component's light DOM withrenderToString.resolveComponentTypesresolves children by calling them, so a nested Stencil SSR component runs its full render (hydrate module andhtml-react-parser) inside its parent's light DOM pass, and React renders the same children again in the final output. Server time doubles with each nesting level.Measured with this repo's
example-project/component-library(Node 24, React 19.1, warm medians):These components are small. In a design system with heavier components, the same shape (30 buttons under three layout wrappers) went from 1,674 ms to 318 ms.
Issue URL: #849
What is the new behavior?
createComponentmarks the async component it returns with a symbol carrying its options.resolveTypemeets a marked component, it returns a plain function component that renders the tag with the same attributes and passes children through. The child's own render happens once, when React renders the real children.splitPropshelper used by both paths.Does this introduce a breaking change?
Other information
A parent still sees its nested children as tags with attributes, slot names and text, which is what the light DOM pass is for. The remaining per-level cost is the parent's hydrate call rendering the bare nested tags;
excludeComponentscould remove that later but changes what the parent sees, so it is left out here.