Skip to content

feat(hydrate): cache platform closure and add opt-in window reuse - #6794

Draft
Armand-Lluka wants to merge 1 commit into
stenciljs:mainfrom
Armand-Lluka:test/hydrate-platform-reuse
Draft

feat(hydrate): cache platform closure and add opt-in window reuse#6794
Armand-Lluka wants to merge 1 commit into
stenciljs:mainfrom
Armand-Lluka:test/hydrate-platform-reuse

Conversation

@Armand-Lluka

@Armand-Lluka Armand-Lluka commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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).

What is the current behavior?

GitHub Issue Number: N/A

What is the new behavior?

Documentation

Does this introduce a breaking change?

  • Yes
  • No

Testing

Other information

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).
Copilot AI review requested due to automatic review settings July 27, 2026 12:12
@Armand-Lluka
Armand-Lluka requested a review from a team as a code owner July 27, 2026 12:12
@Armand-Lluka
Armand-Lluka marked this pull request as draft July 27, 2026 12:13
@Armand-Lluka
Armand-Lluka force-pushed the test/hydrate-platform-reuse branch from 872bc7a to 3a28bcb Compare July 27, 2026 12:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.reuseWindow to reuse a per-process MockWindow (keyed by serializeShadowRoot) for string-input hydrateDocument() / renderToString(), serializing renders through a promise queue.
  • Update public type declarations/documentation to describe the new reuseWindow option.

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

  • runRender only catches synchronous errors. If render(reusedWin, ...) ever rejects, the cached window won't be evicted/closed and the rejection will propagate to callers (unlike other error paths which return a HydrateResults). It would be safer to attach a .catch() to perform the same cleanup + renderCatchError and always resolve results.
          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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants