Skip to content

With isolate: false, each file's vi.mock factory is never released, so worker memory grows per file #11495

Closed

Description

Describe the bug

With isolate: false, every test file's vi.mock() factory stays reachable after the file finishes, together with everything the factory closes over. A worker that runs many files therefore grows by the size of each file's mock-factory scope, and on a large jsdom suite the worker eventually dies with Worker exited unexpectedly.

Mocking itself behaves correctly: each file sees its own mock. Only the memory is never released. vi.resetModules() at the start of each file does not change it. With isolate: true the heap stays flat.

In the reproduction, each of 30 test files mocks the same module with a factory that closes over ~10 MB. The heap measured at the start of each file, after a forced GC (--expose-gc), on one worker:

config heap used at file start (MB)
isolate: false 19, 28, 38, 47, 57 … 296 (+~9.6 MB per file)
isolate: false + vi.resetModules() per file 19, 28, 38, 47, 57 … 296
isolate: true 19 for every file

In a heap snapshot of a real suite, old copies are retained through EvaluatedModuleNode.meta.mockedModule.factory (VitestModuleRunner.evaluatedModules.idToModuleMap), and through mock configs whose vi.fn() keys are themselves reachable from those factories.

#11092 (shipped in 5.0, closing #9492) already stopped REGISTERED_MOCKS from pinning every vi.fn(). This is the remaining path that keeps earlier files alive when isolation is off; the last comments on #9492 describe the same "nothing gets released under isolate: false" symptom.

Reproduction

https://github.com/alexandrucojocaru-creatopy/vitest-isolate-false-mock-retention

npm install, then npm test; heap.log gets one line per file. RESET=1 npm test adds vi.resetModules(), and npm test -- --isolate is the control. The files, inline for convenience:

package.json

{ "name": "vitest-isolate-false-mock-retention", "private": true, "type": "module",
  "scripts": { "test": "node --expose-gc node_modules/vitest/vitest.mjs run" },
  "devDependencies": { "vitest": "5.0.3" } }

vitest.config.ts

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: { isolate: false, pool: 'threads', maxWorkers: 1, fileParallelism: false, setupFiles: ['./heap.setup.ts'] },
});

heap.setup.ts

import { appendFileSync } from 'node:fs';
import { beforeAll, vi } from 'vitest';

if (process.env.RESET === '1') vi.resetModules();
beforeAll(() => {
  (globalThis as { gc?: () => void }).gc?.();
  appendFileSync('heap.log', `${Math.round(process.memoryUsage().heapUsed / 1048576)}\n`);
});

dep.ts

export const dep = (): string => 'real';

test/t01.test.ts … test/t30.test.ts (identical apart from the name)

import { expect, test, vi } from 'vitest';
import { dep } from '../dep';

// ~10 MB that only this file's mock factory references.
const { payload } = vi.hoisted(() => ({ payload: Array.from({ length: 1_250_000 }, (_, i) => i) }));
vi.mock('../dep', () => ({ dep: () => String(payload.length) }));

test('sees its own mock', () => {
  expect(dep()).toBe('1250000');
});

Expected: the heap stays roughly flat once a file's mocks are no longer registered, as with isolate: true.
Actual: it grows by each file's factory scope for the life of the worker.

System Info

vitest 5.0.3 (also reproduces on 4.1.11)
node 24.11.1
macOS 15 (darwin 24.6.0), arm64
pool: threads

Used Package Manager

npm

Validations

  • Follow our Code of Conduct
  • Read the Contributing Guidelines.
  • Read the docs.
  • Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
  • Make sure this is a Vitest issue and not a framework-specific issue.
  • Check that this is a concrete bug.
  • The provided reproduction is a minimal reproducible example of the bug.

Activity

  1. sheremet-va commented on Oct 5, 2026

    @sheremet-va
    Member

    I think it's fixed by #11197

  2. alexandrucojocaru-creatopy commented on Oct 5, 2026

    @alexandrucojocaru-creatopy
    Author

    Thanks for the quick look. I re-ran the reproduction against the pkg.pr.new build of the #11197 merge commit (npm i vitest@https://pkg.pr.new/vitest@28ef93df22, with @vitest/spy and @vitest/mocker resolving to 28ef93d as well). It still grows by the same amount:

    build heap used at file start (MB)
    5.0.3 19, 28, 38, 47 … 296
    28ef93d (#11197) 19, 28, 38, 47 … 296
    28ef93d with --isolate 19 for every file

    The reproduction creates no vi.fn() at all: the factory returns a plain function, so the spy registry isn't involved. What stays reachable is the vi.mock factory closure itself, through EvaluatedModuleNode.meta.mockedModule.factory in evaluatedModules.idToModuleMap, along with everything that closure captures. Could you reopen it?

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions