Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,21 @@ export function hydrateFactory($stencilWindow, $stencilHydrateOpts, $stencilHydr
${HYDRATE_APP_CLOSURE_START}
`;

/**
* The closure wraps the entire platform (runtime, vdom and every component
* class) so it lexically captures the per-call \`window\`/\`document\`.
* Re-executing it on every \`hydrateApp\` call is a significant fixed cost per
* render, so the evaluated closure is cached on the window object and reused
* whenever the same window is passed in again (e.g. via \`reuseWindow\`).
*/
export const HYDRATE_FACTORY_OUTRO = `
/*hydrateAppClosure end*/
hydrateApp(window, $stencilHydrateOpts, $stencilHydrateResults, $stencilAfterHydrate, $stencilHydrateResolve);
return hydrateApp;
}

hydrateAppClosure($stencilWindow);
if (!$stencilWindow.__stencilHydrateApp) {
$stencilWindow.__stencilHydrateApp = hydrateAppClosure($stencilWindow);
}
$stencilWindow.__stencilHydrateApp($stencilWindow, $stencilHydrateOpts, $stencilHydrateResults, $stencilAfterHydrate, $stencilHydrateResolve);
}
Comment on lines +154 to 158
`;
15 changes: 15 additions & 0 deletions src/declarations/stencil-public-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,21 @@ export interface HydrateDocumentOptions {
* Sets `document.referrer`
*/
referrer?: string;
/**
* Reuse a process-global mock window (one per `serializeShadowRoot` mode) across
* `renderToString()`/`hydrateDocument()` calls that receive an HTML string. This
* avoids re-executing the entire hydrate platform closure (runtime, vdom and all
* component definitions) on every call, which is the dominant fixed cost when
* rendering many small fragments (e.g. one `renderToString` call per component
* instance, as the framework output targets do).
*
* 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 +958 to +963
reuseWindow?: boolean;
/**
* Removes every `<script>` element found in the `document`. Defaults to `false`.
*/
Expand Down
72 changes: 72 additions & 0 deletions src/hydrate/runner/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,44 @@ import { initializeWindow } from './window-initialize';

const NOOP = () => {};

/**
* Process-global windows reused across renders when `reuseWindow` is enabled,
* ONE PER `serializeShadowRoot` mode: scoped serialization permanently ORs
* `shadowNeedsScopedCss` into each component's runtime metadata inside the
* cached hydrate platform, which would otherwise leak into later
* declarative-shadow-dom renders sharing the same window.
*/
const reusableWindows = new Map<string, MockWindow>();

/**
* The reused windows are not concurrency-safe, so renders against them are
* serialized through this promise queue.
*/
let reuseRenderQueue: Promise<unknown> = Promise.resolve();

function getReusableWindow(doc: string, opts: HydrateFactoryOptions): MockWindow {
const modeKey = JSON.stringify(opts.serializeShadowRoot ?? null);
let win = reusableWindows.get(modeKey);
if (win) {
const document = win.document;
/**
* Swap in fresh `<head>`/`<body>` ELEMENTS for every render (never reset via
* `innerHTML = ''`): the runtime's `rootAppliedStyles` WeakMap is keyed on
* the head node, so keeping the node identity would silently drop scoped
* `sc-` styles after the first render.
*/
const newHead = document.createElement('head');
document.documentElement.replaceChild(newHead, document.head);
const newBody = document.createElement('body');
newBody.innerHTML = doc;
document.documentElement.replaceChild(newBody, document.body);
} else {
win = new MockWindow(doc);
reusableWindows.set(modeKey, win);
}
return win;
}

export function streamToString(html: string | any, option?: SerializeDocumentOptions) {
return renderToString(html, option, true);
}
Expand Down Expand Up @@ -85,6 +123,40 @@ export function hydrateDocument(
}

if (typeof doc === 'string') {
if (opts.reuseWindow) {
opts.destroyWindow = false;
opts.destroyDocument = false;

const runRender = (): Promise<HydrateResults> => {
let reusedWin: MockWindow | null = null;
try {
reusedWin = getReusableWindow(doc, opts);
return render(reusedWin, opts, results).then(() => results);
} catch (e) {
if (reusedWin) {
reusableWindows.delete(JSON.stringify(opts.serializeShadowRoot ?? null));
if (reusedWin.close) {
reusedWin.close();
}
}
renderCatchError(results, e);
return Promise.resolve(results);
}
};

const queuedRender = reuseRenderQueue.then(runRender, runRender);
reuseRenderQueue = queuedRender.then(NOOP, NOOP);

if (!asStream) {
return queuedRender;
}
return Readable.from(
(async function* () {
yield (await queuedRender).html;
})(),
);
}

try {
opts.destroyWindow = true;
opts.destroyDocument = true;
Expand Down