vite-mastery

5.3 · difficulty 4/4 · 12 min read

Default Environments: client and ssr

How are Vite's two built-in default environments configured? What are the differences in their default behavior? How do you customize the resolve rules for each?

Vite 8.1RC

The two default environments

Even if you configure no environments at all, Vite provides two built-in default environments:

ts
// Vite's internal equivalent configuration (simplified)
export default defineConfig({
  environments: {
    client: {
      // browser environment
      resolve: {
        conditions: ["browser", "module", "import"],
      },
    },
    ssr: {
      // Node.js server environment
      resolve: {
        conditions: ["node", "module", "import", "require"],
        noExternal: [], // no node_modules bundled by default
      },
    },
  },
})

These two environments cover the needs of traditional SSR applications. If your project uses a standard client + ssr architecture, you do not need to explicitly configure environments.

The client environment

The client environment corresponds to the browser execution context.

Resolve conditions

The client environment's resolve uses the browser condition:

ts
// When importing "some-pkg"
// Vite will preferentially read from package.json:
// "exports": {
//   "browser": "./dist/browser.js",  ← preferred
//   "import": "./dist/index.js",
//   "require": "./dist/index.cjs"
// }

This ensures that the client bundle uses the browser-specific version of a package (for example, a version without Node.js-exclusive APIs).

HMR support

The client environment has full built-in HMR support:

  • Establishes a WebSocket connection with the browser
  • Automatically notifies import.meta.hot callbacks when a module updates
  • Supports accept, dispose, invalidate, and other HMR APIs

Output target

In build mode, the client environment produces a JavaScript bundle targeting the browser.

The ssr environment

The ssr environment corresponds to the Node.js server execution context.

Resolve conditions

The ssr environment uses the node condition:

ts
// When importing "some-pkg"
// Vite SSR will preferentially read:
// "exports": {
//   "node": "./dist/node.js",    ← preferred
//   "import": "./dist/index.js",
// }

This allows the same package to provide different implementations for the browser and Node.js (for example, the crypto module has a native implementation in Node but requires a polyfill in the browser).

External behavior

The ssr environment treats all node_modules packages as external by default — meaning they are not bundled into the SSR bundle, but are instead required directly by Node.js at runtime:

ts
// To bundle certain packages into the SSR output (e.g. ESM-only packages)
ssr: {
  resolve: {
    noExternal: ["some-esm-only-package"],
  },
}

This is the opposite of the client environment's build behavior: the client bundles all imports.

HMR support

The ssr environment's HMR is implemented via a server-side message mechanism (not WebSocket). When a server-side module updates, the ModuleRunner's module cache is marked as invalid, and the next runner.import call reloads the latest version.

Comparing client and ssr environments

Environment API · module boundary visualization

client

Browser environment

ssr

Node.js SSR

rsc

React Server Components

EntryShared (multi-env)Environment-onlyClick a module to inspect details
Dimensionclientssr
Execution targetBrowserNode.js
Primary conditionbrowsernode
node_modulesAll bundledExternal by default
HMR channelWebSocketServer-side message
import.meta.envVITE_* + DEV/PROD/MODESame as client + SSR-specific
Output formatESM (browser)ESM/CJS (Node.js)

Customizing the two default environments

Although you do not need to explicitly configure environments, the following situations require overriding the defaults:

Scenario 1: SSR needs to bundle specific packages

ts
export default defineConfig({
  environments: {
    ssr: {
      resolve: {
        // Some ESM-only packages cannot be directly required by Node.js at runtime and need to be bundled
        noExternal: ["marked", "shiki", "@some/esm-package"],
      },
    },
  },
})

Scenario 2: client needs additional resolve conditions

ts
export default defineConfig({
  environments: {
    client: {
      resolve: {
        // Append a condition to support a specific package format
        conditions: ["browser", "module", "import", "custom-condition"],
      },
    },
  },
})

Scenario 3: adding environment-specific plugins to ssr

ts
export default defineConfig({
  environments: {
    ssr: {
      plugins: [
        // A plugin that only runs in the ssr environment
        ssrOnlyPlugin(),
      ],
    },
  },
})

Migration: from the old SSR API to the Environment API

If you have SSR implemented using Vite 7's ssrLoadModule, the basic migration approach to the Environment API is:

The recommended approach after migration:

ts
// Vite 8 recommended style (conceptual illustration — refer to official docs for the exact API)
const vite = await createServer({
  server: { middlewareMode: true },
})

// Use the ModuleRunner from the ssr Environment
const runner = createModuleRunner(vite.environments.ssr)

app.use("*", async (req, res) => {
  const { render } = await runner.import("/src/entry-server.tsx")
  const html = await render(req.url)
  res.send(html)
})

Self-check

  1. Why does the client environment prefer the browser condition over node? Give a real-world example of the difference.
  2. What is the reason the ssr environment externalizes all node_modules by default? When should you use noExternal to override this?
  3. If a package's package.json has browser, node, and import export conditions simultaneously, which one is used in the client environment and which in the ssr environment?
  4. How does the HMR behavior of Vite 7's ssrLoadModule differ from the ModuleRunner in Vite 8's Environment API?
ts
// The following package has this exports configuration in its package.json:
// {
//   "exports": {
//     ".": {
//       "browser": "./dist/browser.esm.js",
//       "node": "./dist/node.cjs.js",
//       "import": "./dist/index.esm.js",
//       "require": "./dist/index.cjs.js"
//     }
//   }
// }

// Q1: Which file does Vite use when importing this package in the client environment?
// Q2: Which file is used when importing in the ssr environment?
// Q3: If the ssr environment is configured with noExternal: ["this-package"],
//     is this package inlined or referenced via require in the ssr bundle?
// Q4: If you were to add a Cloudflare Worker environment,
//     which conditions should you use? Why?
export default defineConfig({
  environments: {
    worker: {
      resolve: {
        conditions: [/* TODO */],
      },
    },
  },
})