Skip to content

Add opt-in worker-scoped Nuxt app reuse for Vitest #1750

Description

@IlyaSemenov

Describe the feature

Add an opt-in mode that initializes one Nuxt app per Vitest worker and reuses it across test files assigned to that worker.

For example:

export default defineVitestConfig({
  test: {
    environment: "nuxt",
    pool: "threads",
    isolate: false,
    environmentOptions: {
      nuxt: {
        appIsolation: "worker",
      },
    },
  },
})

The current file-scoped lifecycle should remain the default.

In worker-scoped mode:

  • setupNuxt() runs once per worker;
  • files assigned to the same worker reuse the Nuxt app;
  • different workers create separate Nuxt apps;
  • a rejected setup promise is removed so initialization can be retried.

The option name is only a suggestion.
Any public API providing the same lifecycle would solve the problem.

Motivation

The internal runtime entry currently registers a beforeAll hook that calls setupNuxt():

beforeAll(async () => {
  await setupNuxt()
})

Vitest evaluates setup files for every test file, so Nuxt is initialized again for each file even with isolate: false.

This adds a large fixed cost to every component-test file.
In a real Nuxt application, warm files spent approximately 300–400 ms on this overhead before their component tests ran.

With one Vitest worker, the complete 12-file, 22-test suite took approximately:

file-scoped setup:   19.6 s
worker-scoped setup:  6.6 s

After worker reuse was enabled, subsequent files generally completed in milliseconds, depending on their imports and test logic.

A separate benchmark ran 1,000 simple component tests after a single Nuxt startup.
Each test mounted a component using the application's normal helper and performed the normal state cleanup.
The test phase took approximately 4.2 seconds, or 4.2 ms per mount-and-reset cycle.

These measurements distinguish the fixed Nuxt initialization cost paid by every file from the much smaller cost of mounting and cleaning up a simple component after Nuxt is already running.

The tested application uses Nuxt 4.5.0, @nuxt/test-utils 4.0.3, Vitest 4.1.10, Vite 8.1.4, and Vue Test Utils 2.4.11.
Its tests use the full Nuxt environment, including auto-imports, component registration, plugins, Nuxt UI, Pug, and normal Vite transforms.

Existing workarounds

Both known workarounds use isolate: false and cache the setupNuxt() promise on the worker's globalThis.

Package-manager patch

Pin @nuxt/test-utils to an exact version and patch dist/runtime/entry.mjs.

The patched hook preserves the default behavior unless worker reuse is explicitly enabled:

const workerSetupEnabledKey = Symbol.for(
  "nuxt-test.worker-setup-enabled",
)
const setupKey = Symbol.for("nuxt-test.worker-setup")

beforeAll(async () => {
  if (!globalThis[workerSetupEnabledKey]) {
    await setupNuxt()
    return
  }

  globalThis[setupKey] ??= setupNuxt().catch((error) => {
    delete globalThis[setupKey]
    throw error
  })

  await globalThis[setupKey]
})

The application enables worker reuse from its test setup:

Reflect.set(
  globalThis,
  Symbol.for("nuxt-test.worker-setup-enabled"),
  true,
)

This approach works reliably, but it patches a private compiled entry and requires reconciling the patch whenever @nuxt/test-utils is upgraded.

Vite plugin

A Vite plugin can intercept the resolved @nuxt/test-utils/dist/runtime/entry.mjs module and replace its direct setupNuxt() call with the same worker-global promise guard.

This avoids modifying the installed package, but it still depends on a private module path, the generated source shape, and Vite transform ordering.

Isolation requirements

Worker reuse means that files in the same worker share one Nuxt app and one module graph.
It must therefore be explicit and opt-in.

Startup mocks cannot vary between files after the app has initialized.
In particular, file-local mockNuxtImport() calls are hoisted module mocks and are not reliably file-local with isolate: false.
Tests that need different behavior can use stable worker-level mocks backed by per-test implementations.

Applications must also clean up mutable state after each test.
The working implementation unmounts Vue wrappers and resets Nuxt state and data, the router, Nuxt errors, cookies, browser storage, timers, global stubs, test-created DOM nodes, mocks, and application-specific registries.

This cleanup is application-specific and does not need to be part of the initial worker lifecycle API.
The documentation should state that consumers are responsible for isolating tests when they enable app reuse.

Watch mode may require restarting or invalidating a worker when Nuxt startup modules change.
It would be sufficient for the initial API to document its watch-mode behavior or limitations.

Possible implementation

The existing runtime entry could cache its setup promise when worker isolation is selected:

import environmentOptions from "nuxt-vitest-environment-options"
import { beforeAll } from "vitest"

import { setupNuxt } from "./shared/nuxt"

const workerSetupKey = Symbol.for(
  "@nuxt/test-utils:worker-setup",
)

if (
  typeof window !== "undefined"
  && window.__NUXT_VITEST_ENVIRONMENT__
) {
  beforeAll(async () => {
    if (environmentOptions.nuxt?.appIsolation !== "worker") {
      await setupNuxt()
      return
    }

    const workerGlobal = globalThis as typeof globalThis & {
      [workerSetupKey]?: Promise<unknown>
    }

    workerGlobal[workerSetupKey] ??= setupNuxt().catch((error) => {
      delete workerGlobal[workerSetupKey]
      throw error
    })

    await workerGlobal[workerSetupKey]
  })
}

This is only an example of the required lifecycle.

Suggested tests

  1. The default mode initializes Nuxt once per test file.
  2. Worker mode with isolate: false initializes Nuxt once for multiple files in one worker.
  3. Multiple workers initialize separate Nuxt apps.
  4. A rejected setup promise can be retried.
  5. File-specific startup mocks continue to work in the default mode.
  6. The behavior or limitation of worker mode in watch and browser modes is documented and tested where applicable.

Additional information

  • Would you be willing to help implement this feature?
  • Could this feature be implemented as a module?

Final checks

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions