Skip to content

[SSR][reactivity] Large memory spikes under sustained server rendering; explicit per-request effectScope stop reduces peak dramatically #34324

@janswist

Description

@janswist

Summary

In a Nuxt SSR app running on Vue 3.5.13, we see very high process RSS spikes under sustained load.

  • Without custom per-request scope cleanup, RSS peaks at ~2.5-2.8 GB.
  • When we explicitly track and stop all effect scopes created during a request, peak drops to ~436 MB.

This suggests potential issues around request-bound reactivity/effect scope lifecycle cleanup under SSR.

Environment

  • Vue: 3.5.13
  • Runtime context: SSR through Nuxt/Nitro (nuxt@3.17.5, nitro@2.11.12)
  • Node: 22.x
  • Platform tested: macOS (Apple Silicon)

Load profile

  • npx autocannon -d 60 -c 30 http://127.0.0.1:3100/
  • RSS sampled every second

A/B observed data

Variant BEFORE_KB MAX_KB AFTER_KB IDLE20_KB
default (no extra cleanup) 82704 2825840 1916608 48880
custom scope cleanup 38576 435936 368784 110016

Additional default run:

  • MAX_KB=2770272, AFTER_KB=1366000, IDLE20_KB=50416

What our custom cleanup does

As an experiment, we patched build output to:

  1. Track all effect scopes created during SSR request execution using AsyncLocalStorage.
  2. After request render, stop tracked scopes in reverse order.

With this approach, peak memory dropped significantly.

Why we suspect Vue reactivity/effect lifecycle

The strongest signal is that explicit post-request scope.stop() for all request-created scopes changes memory profile dramatically.

Package toggles in surrounding stack did not materially remove the problem:

  • @pinia/nuxt@0.5.5: MAX_KB=2669616
  • @nuxtjs/i18n@10.0.6: MAX_KB=2731184
  • experimental.asyncContext=true: MAX_KB=2513312

Expected behavior

Request-scoped reactive effects should be fully released/disposed after SSR render completion so that sustained traffic does not produce multi-GB peak RSS in this scenario.

Actual behavior

Under sustained SSR traffic, process RSS reaches ~2.5-2.8 GB unless we forcibly stop tracked request scopes.

Questions for Vue maintainers

  1. Are there known SSR edge cases where request-created effectScope instances can outlive request lifecycle unintentionally?
  2. Is there a recommended, supported way to ensure complete disposal of request-scoped effects in SSR integrations?
  3. Are there diagnostics/hooks in Vue reactivity internals we can use to confirm orphaned effects in production-like workloads?

If helpful, we can provide logs and a minimized reproduction (likely via Nuxt SSR harness) focusing only on the reactivity lifecycle aspect.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions