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?
The two default environments
Even if you configure no environments at all, Vite provides two built-in default environments:
// 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:
// 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.hotcallbacks 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:
// 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:
// 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
client
Browser environment
ssr
Node.js SSR
rsc
React Server Components
| Dimension | client | ssr |
|---|---|---|
| Execution target | Browser | Node.js |
| Primary condition | browser | node |
| node_modules | All bundled | External by default |
| HMR channel | WebSocket | Server-side message |
| import.meta.env | VITE_* + DEV/PROD/MODE | Same as client + SSR-specific |
| Output format | ESM (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
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
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
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:
// 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
- Why does the
clientenvironment prefer thebrowsercondition overnode? Give a real-world example of the difference. - What is the reason the
ssrenvironment externalizes all node_modules by default? When should you usenoExternalto override this? - If a package's
package.jsonhasbrowser,node, andimportexport conditions simultaneously, which one is used in theclientenvironment and which in thessrenvironment? - How does the HMR behavior of Vite 7's
ssrLoadModulediffer from theModuleRunnerin Vite 8's Environment API?
// 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 */],
},
},
},
})