Skip to content

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

Closed

Description

@SebastianKohler

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:

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

  • Node.js: 24.20.0
  • npm: 11.19.0
  • @ionic/core@9.0.4
  • @ionic/angular-server@9.0.4

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:

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:

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: [].

Activity

  1. SebastianKohler commented on Sep 23, 2026

    @SebastianKohler
    ContributorAuthor

    Upstream Stencil update

    Stencil has opened stenciljs/core#6903 to fix stenciljs/core#6901.

    The PR confirms the reported root cause: Ionic's global script calls setMode() during every server render, while Stencil currently appends each resolver to the module-scoped modeResolutionChain.

    The upstream implementation differs from the reset suggested here:

    • setMode() will replace the existing resolver instead of appending another resolver.
    • Per-call opts.modes overrides will be applied after the global script and restored after rendering.
    • Stencil rejected an unconditional reset at the start of renderToString() because concurrent renders, such as React/Next sibling renders using Promise.all, could clear state still needed by another render.

    Consequently, modes: [] remains a possible interim workaround for Ionic releases containing the affected Stencil runtime, but it is not the intended upstream fix. The final fix for Ionic will be to rebuild and publish @ionic/core/hydrate using a Stencil release containing #6903, since updating an application's @stencil/core dependency does not replace the runtime embedded in Ionic's precompiled hydrate bundle.

  2. changed the title [-]bug: SSR hydrate bundle retains every render; pass `modes: []` as Stencil workaround[/-] [+]bug: SSR memory leak: hydrate bundle retains a mode resolver on every render[/+] on Sep 23, 2026
  3. SebastianKohler commented on Sep 23, 2026

    @SebastianKohler
    ContributorAuthor

    Update: stenciljs/core#6903 has been merged and the fix is now available in @stencil/core 4.45.1.

    Ionic can now incorporate the upstream fix by rebuilding and publishing @ionic/core/hydrate with Stencil 4.45.1 or later.

  4. ShaneK commented on Sep 23, 2026

    @ShaneK
    Member

    Hey @SebastianKohler! The stencil/core update was applied before we released Ionic Framework v9.0.5, so you should be good now! Thanks for following up on this

  5. SebastianKohler commented on Sep 24, 2026

    @SebastianKohler
    ContributorAuthor

    Thanks @ShaneK, however, the @stencil/core 4.45.1 upgrade PR is still open with a pending review requirement and doesn't seem to have made it to the 9.0.5 release: #31472

  6. reopened this on Sep 25, 2026
  7. ShaneK commented on Sep 25, 2026

    @ShaneK
    Member

    Welp, that was dumb of me. Sorry about that, I've definitely sent it to the merge queue this time so it'll end up in the next release 🤦‍♂️

  8. SebastianKohler commented on Oct 2, 2026

    @SebastianKohler
    ContributorAuthor

    Closed by #31472, included in the 9.0.6 release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions