feat(hydrate): cache platform closure and add opt-in window reuse - #6794
Draft
Armand-Lluka wants to merge 1 commit into
Draft
feat(hydrate): cache platform closure and add opt-in window reuse#6794Armand-Lluka wants to merge 1 commit into
Armand-Lluka wants to merge 1 commit into
Conversation
The hydrate factory wraps the entire platform (runtime, vdom and every component class) in hydrateAppClosure() so it lexically captures the per-call window, and re-executes that closure on EVERY renderToString call. For component libraries this is the dominant fixed cost per render (framework output targets call renderToString once per component instance). Two changes: 1. The factory now caches the evaluated closure on the window object and reuses it whenever the same window is passed again. Transparent for fresh-window renders. 2. New opt-in HydrateDocumentOptions.reuseWindow: the string-input path of hydrateDocument()/renderToString() reuses a process-global MockWindow, one per serializeShadowRoot mode (scoped serialization permanently ORs shadowNeedsScopedCss into component metadata inside the cached closure). Fresh head/body ELEMENTS are swapped in per render (never innerHTML='') because rootAppliedStyles is keyed on the head node. Renders are serialized through an internal promise queue since the shared window is not concurrency-safe. Measured 2.2x per-render speedup on a 2-component test app; scales with component count (~4.3x on a 475-component library).
Armand-Lluka
marked this pull request as draft
July 27, 2026 12:13
Armand-Lluka
force-pushed
the
test/hydrate-platform-reuse
branch
from
July 27, 2026 12:18
872bc7a to
3a28bcb
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This PR optimizes hydrate SSR performance by avoiding repeated evaluation of the hydrate platform closure and (optionally) reusing a process-global mock window for string-based renders.
Changes:
- Cache the evaluated
hydrateAppClosure()result on the provided window and reuse it when the same window is used again. - Add
HydrateDocumentOptions.reuseWindowto reuse a per-processMockWindow(keyed byserializeShadowRoot) for string-inputhydrateDocument()/renderToString(), serializing renders through a promise queue. - Update public type declarations/documentation to describe the new
reuseWindowoption.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/hydrate/runner/render.ts | Adds reusable MockWindow caching and a serialized render queue behind reuseWindow. |
| src/declarations/stencil-public-compiler.ts | Documents/exposes the new reuseWindow?: boolean public option. |
| src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts | Changes generated hydrate factory to cache the evaluated hydrate closure on window. |
Comments suppressed due to low confidence (1)
src/hydrate/runner/render.ts:135
runRenderonly catches synchronous errors. Ifrender(reusedWin, ...)ever rejects, the cached window won't be evicted/closed and the rejection will propagate to callers (unlike other error paths which return aHydrateResults). It would be safer to attach a.catch()to perform the same cleanup +renderCatchErrorand always resolveresults.
reusedWin = getReusableWindow(doc, opts);
return render(reusedWin, opts, results).then(() => results);
} catch (e) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| if (typeof doc === 'string') { | ||
| if (opts.reuseWindow) { |
Comment on lines
+958
to
+963
| * The reused window is not concurrency safe, so renders are serialized through | ||
| * an internal queue. Intended for rendering HTML fragments | ||
| * (`fullDocument: false`); one known output difference is that hydration | ||
| * annotation counters (e.g. `<!--r.N-->`) become unique across the process | ||
| * instead of restarting at 1 per call. Defaults to `false`. | ||
| */ |
Comment on lines
+154
to
158
| if (!$stencilWindow.__stencilHydrateApp) { | ||
| $stencilWindow.__stencilHydrateApp = hydrateAppClosure($stencilWindow); | ||
| } | ||
| $stencilWindow.__stencilHydrateApp($stencilWindow, $stencilHydrateOpts, $stencilHydrateResults, $stencilAfterHydrate, $stencilHydrateResolve); | ||
| } |
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.
The hydrate factory wraps the entire platform (runtime, vdom and every component class) in hydrateAppClosure() so it lexically captures the per-call window, and re-executes that closure on EVERY renderToString call. For component libraries this is the dominant fixed cost per render (framework output targets call renderToString once per component instance).
Two changes:
The factory now caches the evaluated closure on the window object and reuses it whenever the same window is passed again. Transparent for fresh-window renders.
New opt-in HydrateDocumentOptions.reuseWindow: the string-input path of hydrateDocument()/renderToString() reuses a process-global MockWindow, one per serializeShadowRoot mode (scoped serialization permanently ORs shadowNeedsScopedCss into component metadata inside the cached closure). Fresh head/body ELEMENTS are swapped in per render (never innerHTML='') because rootAppliedStyles is keyed on the head node. Renders are serialized through an internal promise queue since the shared window is not concurrency-safe.
Measured 2.2x per-render speedup on a 2-component test app; scales with component count (~4.3x on a 475-component library).
What is the current behavior?
GitHub Issue Number: N/A
What is the new behavior?
Documentation
Does this introduce a breaking change?
Testing
Other information