bug: SSR memory leak: hydrate bundle retains a mode resolver on every render

Author: SebastianKohlerCreated Sep 22, 2026Updated Sep 25, 2026
Labelstriage

Prerequisites

Ionic Framework Version

v9.x, v8.x

Current Behavior

Every Ionic server-side hydrate call retains a Stencil hydrate closure. In a production Angular SSR application, memory grows with every request until the container is out of memory.

Expected Behavior

After a server-side render completes, per-render state should be eligible for garbage collection. Repeated hydrateDocument() calls should reach a stable memory level rather than retaining another closure and increasing live heap after every render.

Steps to Reproduce

The attached minimal reproduction uses @ionic/core/hydrate directly, with no Angular application or client hydration. It uses the same fallback options that @ionic/angular-server selects when the server DOM does not implement attachShadow().

Run:

bash
npm install
npm run repro

The script performs 500 identical hydrateDocument() calls and forces a full V8 garbage collection after every completed call.

Observed with Node 24.20.0 and Ionic 9.0.4:

render 1:   heapUsed 6.54 MiB
render 100: heapUsed 33.58 MiB
render 200: heapUsed 59.78 MiB
render 300: heapUsed 85.98 MiB
render 400: heapUsed 112.16 MiB
render 500: heapUsed 138.49 MiB

Code Reproduction URL

https://github.com/SebastianKohler/ionic-ssr-memory-leak-repro

Ionic Info

The regression is also present in Ionic 8.6.0 and later. Ionic 8.5.9, whose hydrate bundle embeds Stencil Hydrate Platform 4.20.0, does not reproduce it.

Additional Information

Root cause

This is caused by Stencil's module-scoped modeResolutionChain, introduced by stenciljs/core#5953.

Ionic's generated global runtime registers its ios/md mode resolver during every hydrate render. Stencil clears the shared chain only when the caller supplies a modes array. @ionic/angular-server currently calls hydrateDocument() without modes, so every SSR request appends and retains another resolver closure.

The corresponding Stencil bug is stenciljs/core#6901.

Suggested Ionic workaround

Add modes: [] to the options passed to hydrateDocument() in @ionic/angular-server:

typescript
return hydrateDocument(doc, {
  modes: [],
  ...(supportsNativeAttachShadow
    ? { clientHydrateAnnotations: false }
    : {
        serializeShadowRoot: 'scoped',
        clientHydrateAnnotations: true,
      }),
  excludeComponents: [/* existing list */],
});

This activates Stencil's existing reset branch before Ionic registers its normal resolver. It does not enable or otherwise change Angular client hydration, and it does not require reverting the attachShadow() fallback.

Control:

bash
npm run repro:workaround

This adds only modes: [] and keeps live heap approximately flat:

render 1:   heapUsed 6.54 MiB
render 100: heapUsed 7.70 MiB
render 200: heapUsed 7.75 MiB
render 300: heapUsed 7.80 MiB
render 400: heapUsed 7.81 MiB
render 500: heapUsed 7.99 MiB

Options for Ionic

@ionic/core/hydrate is a precompiled bundle containing its own Stencil hydrate runtime, so an application-level override of @stencil/core does not replace the affected code.

Ionic can address the leak in either or both of these ways:

  1. Add modes: [] in @ionic/angular-server now as the targeted workaround demonstrated above.
  2. Once stenciljs/core#6901 is fixed, update the Stencil version used to build @ionic/core and publish a rebuilt hydrate bundle.

These changes are compatible: Ionic can ship the workaround immediately and later update its embedded Stencil runtime. After updating Stencil, the explicit modes: [] reset can either remain as a defensive measure or be removed if it is redundant with the upstream fix.

Version comparison

The same Node-only reproduction was run for 500 identical hydrateDocument() calls, with a full garbage collection after every call:

Ionic version Configuration Heap after render 1 Heap after render 500 Growth
8.5.9 (last unaffected release, Stencil 4.20.0) No workaround 6.30 MiB 7.31 MiB +1.01 MiB
9.0.4 (Stencil 4.45.0) No workaround 6.54 MiB 138.49 MiB +131.95 MiB
9.0.4 (Stencil 4.45.0) modes: [] 6.54 MiB 7.99 MiB +1.45 MiB

All three runs use serializeShadowRoot: 'scoped' and clientHydrateAnnotations: true. The third run differs from the second only by adding modes: [].

Source: ionic-team/ionic-framework