Skip to content

Vite 6 Environment API: A Guide to Multi-Target Builds

What the Vite Environment API Actually Changes

Vite 6's Environment API lets one config define SSR, edge, and client builds as named environments instead of separate Vite instances bolted together.

· · 4 min read

Quick Take

Before Vite 6, running the same app for client, SSR, and an edge runtime meant three separate build configs held together with conditionals. The Environment API replaces that with one config and named environments.

I maintained a project with three Vite config files, one for the client bundle, one for SSR, one for a Cloudflare Workers edge deploy, each duplicating most of the same plugin list with small variations. Every plugin update meant editing three files and hoping I didn't miss one. The Environment API collapsed that into a single config with three named environments.

TL;DR: Vite 6's Environment API lets one config define multiple build targets, client, SSR, edge, as named "environments," each with its own module resolution, plugins, and output, all within a single Vite instance instead of separate config files stitched together. Most app developers won't touch it directly, their framework (Astro, Remix, and others) already uses it internally, but understanding it explains why multi-target builds got noticeably faster and more consistent in 2026.

The Problem: One App, Multiple Runtimes

A typical SSR app runs in at least two different JavaScript environments: the browser, where React hydrates and DOM APIs exist, and the server (Node.js, or an edge runtime like Cloudflare Workers), where there's no window and module resolution rules can differ. Before Vite 6, handling both meant either:

  • Running two separate Vite dev servers and gluing their output together, or
  • One config file full of if (isSSR) { ... } else { ... } branches for plugins, aliases, and build targets.

Neither scales cleanly once you add a third target, an edge runtime with its own module resolution quirks (no Node built-ins, different exports conditions in package.json).

Defining Named Environments

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  environments: {
    client: {
      build: {
        outDir: 'dist/client',
        rollupOptions: { input: './src/entry-client.tsx' },
      },
    },
    ssr: {
      build: {
        outDir: 'dist/server',
        rollupOptions: { input: './src/entry-server.tsx' },
        ssr: true,
      },
      resolve: {
        conditions: ['node'],
      },
    },
    edge: {
      build: {
        outDir: 'dist/edge',
        rollupOptions: { input: './src/entry-edge.tsx' },
      },
      resolve: {
        conditions: ['worker', 'browser'],
        noExternal: true, // bundle everything, edge runtimes often can't resolve node_modules at runtime
      },
    },
  },
  plugins: [react()],
});

Each environment gets its own resolve.conditions, which determines which package.json exports field wins when a dependency ships different code for Node versus the browser versus edge runtimes. That's the detail that used to cause the most subtle bugs, a package resolving to its Node build inside an edge bundle, then crashing at runtime on a missing fs import.

Why This Matters Even If You Don't Write the Config

If you use Astro, the framework already defines environments like this internally for its SSR adapters (Node, Cloudflare, Vercel edge). The practical result: switching your Astro output mode or adapter no longer means Vite re-resolving your entire dependency graph from scratch with a different config, it reuses shared module graph analysis across environments where possible. That's part of why cold starts and rebuild times on multi-target Astro projects got faster through 2026 without any change to your own astro.config.mjs.

Running an Environment-Specific Dev Server Programmatically

For tooling authors (framework builders, test runners), the Environment API exposes a programmatic way to run a specific environment's module graph without spinning up a full dev server per target:

import { createServer } from 'vite';

const server = await createServer({
  configFile: './vite.config.ts',
});

// Run a module through the 'ssr' environment specifically
const ssrEnv = server.environments.ssr;
const mod = await ssrEnv.transformRequest('/src/entry-server.tsx');

Most app developers never call this directly, it's the layer test runners like Vitest use to execute your code the same way Vite would at runtime, including SSR-specific transforms, instead of running your source through a generic Node require.

Old Way vs Modern Way

TaskVite 5 and earlierVite 6 Environment API
Client + SSR buildTwo config files or heavy conditionalsTwo named environments in one config
Edge runtime module resolutionManual resolve.conditions overrides per build scriptPer-environment resolve.conditions
Dependency exports mismatchesDiscovered at runtime, often in productionCaught at build time per environment
Plugin list maintenanceDuplicated across config filesShared plugin list, per-environment overrides only where needed

When to Reach for This Directly

Most teams shipping a single-target SPA or a standard SSR app through a mainstream meta-framework never need to write environment configs by hand, the framework owns that layer. Reach for it directly when you're building a custom deploy target the framework doesn't support yet, or when you're authoring a Vite plugin or framework yourself and need consistent module resolution across more than one runtime.

Conclusion

The Environment API doesn't change what Vite builds, it changes how cleanly you can describe multiple targets from one place. If you've ever debugged why a dependency resolved differently in your SSR bundle than your client bundle, that's exactly the class of bug per-environment resolve.conditions is built to catch at build time instead of in production.

Frequently Asked Questions

What problem does the Vite Environment API solve?
Before Vite 6, an app that shipped a client bundle, a server-side rendering bundle, and an edge runtime bundle needed either three separate Vite config files or one config full of environment-specific conditionals for plugins, resolve aliases, and build targets. The Environment API lets a single config define each target as a named 'environment' with its own resolve conditions, plugins, and build output, all resolved in one Vite instance.
Do I need to migrate an existing Vite project to use it?
Not unless you're building for multiple runtime targets. A standard client-only SPA or a simple SSR setup using vite-plugin-ssr or a meta-framework like Astro or Next.js doesn't need to touch the Environment API directly, those frameworks already configure it for you under the hood. It matters when you're hand-rolling a build for more than one runtime yourself.
Which frameworks already use the Environment API internally?
Astro, Remix, and several edge-first frameworks adopted it as their SSR/client build backbone starting in 2025 and through 2026, which is why most app developers never interact with the raw API, they get its benefits (faster cold starts, consistent module resolution across targets) without writing environment configs themselves.