#23493·Vite

Dev server restarts retain retired environments through options.previousEnvironments

Author: jav-edCreated Sep 15, 2026Updated Sep 16, 2026

Describe the bug

Repeated server.restart() calls retain retired environment/configuration graphs through _createServer's options.previousEnvironments. The live server keeps its initialization/listen closure, which still references options for options.listen. That options object continues to hold the previous environments after their initialization handoff is complete.

This creates a chain across server generations and can retain old module graphs and plugin caches. In a larger development app this contributed to hundreds of MB of retained heap per full restart and eventual Node heap exhaustion. The reproduction below needs no framework, application code, file watcher, HTTP listener, or browser.

Reproduction

Minimal runnable reproduction, exact package versions, and workarounds.

There is also an independent native callback retention bug in Rolldown #10887. The first workaround step below removes that independent root so the test measures Vite's restart-reference chain.

Steps to reproduce

git clone https://gist.github.com/7db63deba8526a8810164775e9e5f330.git memory-retention-repro
cd memory-retention-repro
npm install
node apply-workaround.mjs rolldown
npm run repro:vite

Expected: the four retired client environments and their configs become collectible while the current public server remains running.

Actual on Vite 8.3.0:

{ checked: 8, retained: 8 }
AssertionError: The running Vite server retains every retired generation

The test holds only WeakRefs to the retired objects and crosses eight event-loop turns with explicit GC before checking. It starts measuring after the first restart because Vite preserves accessors on the original public server object; the assertion concerns subsequent generations. Closing the entire server before checking would hide this growing chain.

Local fix and result

After the awaited environment initialization in _createServer (packages/vite/src/node/server/index.ts), release the initialization-only reference:

options.previousEnvironments = undefined

The previous instance has already been passed to each environment's awaited init at this point. The reproduction includes the equivalent small patch against the published package:

node apply-workaround.mjs vite
npm run repro:vite

Result:

{ checked: 8, retained: 0 }

The before/after test gives the same results on Node 22.22.3, 24.18.0, and 26.7.0. We also applied this cleanup locally to Vite 8.1.4, added an uncached lifecycle regression check, and verified real browser reloads, typecheck, and production builds alongside the separate Rolldown correction. This regression check should inshallah help catch future changes that reintroduce the retention chain.

System Info

OS: Linux x86_64
Node: 22.22.3 / 24.18.0 / 26.7.0
Vite: 8.3.0 (also observed with 8.1.4)
Rolldown: 1.2.8
Framework plugins: none in reproduction
Configuration: configFile=false, middlewareMode=true, watch=null, ws=false

Used Package Manager

npm

Related issues checked

  • #22992 addresses an in-flight HMR update racing a restart. This reproduction restarts sequentially and does not dispatch HMR updates.
  • #21716 addresses rejected URL-resolution promises. This reproduction resolves an existing package successfully.
  • I found no existing issue/PR covering retention through options.previousEnvironments; the matching reference is still present in the current source.