Production builds, environment config and deployment

What Vite emits, route-level code splitting, why VITE_ variables are public, SPA rewrites on static hosts, bundle analysis and error reporting.

Build output and code splitting

import { lazy } from 'react'

// route-level splitting is the highest-value split: the chunk is fetched on first visit
const Settings = lazy(() => import('./routes/Settings'))

// keep heavy, rarely used pieces out of the entry chunk too
const Chart = lazy(() => import('./components/Chart'))

// Vite creates a chunk per dynamic import, named from the module path,
// so a large feature becomes a download the user pays for only if they use it
PathWhat it isWhat to do
dist/index.htmlThe shell with hashed asset linksServe it; never edit it by hand
dist/assets/*.jsContent-hashed chunksCache for a year - the name changes with the content
dist/assets/*.cssExtracted stylesheetsSame caching rule as the scripts
public/Files copied verbatimFavicons, robots.txt, _redirects
package-lock.jsonExact dependency versionsCommit it and install with npm ci in CI
dist/The only artefact to deployBuild it in CI, never on the server

Environment variables that behave

// src/config.js - read once, validate once, fail loudly
const apiUrl = import.meta.env.VITE_API_URL
if (!apiUrl) {
  throw new Error('VITE_API_URL is required')
}

export const API_URL = apiUrl
export const isProduction = import.meta.env.PROD
  • Anything prefixed VITE_ is inlined into the bundle as a literal string, so it is public. A key, a token or a connection string must never live there.
  • Vite substitutes the expression at build time, so changing an environment variable requires a new build - editing it on the host afterwards has no effect on an already-built artefact.
  • Values are per-build, not per-request: use two builds when staging and production need different API URLs, or read runtime configuration from the server instead.
  • Validate the variables at start-up so a missing value fails immediately rather than at the first request.

Rewrites, analysis and monitoring

# inspect what is actually inside each chunk before shipping
npx vite-bundle-visualizer

# a build with the production environment, so the analysis matches production
npm run build -- --mode production
// report runtime errors before users do
import * as Sentry from '@sentry/react'

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  environment: import.meta.env.MODE,
  tracesSampleRate: 0.1
})
  1. Build once per commit and treat the output as immutable; the hash in each filename makes that safe to cache.
  2. Add the SPA rewrite so unknown paths return the shell, and keep static assets resolving before the rewrite applies.
  3. Check the bundle after adding a dependency: a date library or an icon set can double the entry chunk in one commit.
  4. Send source maps to the error reporter, not to the public site, so stack traces stay readable without exposing source.
  5. Roll back by re-pointing the host at the previous artefact rather than rebuilding an older commit.
⚠️
A client-side router breaks every deep link and refresh unless the host rewrites unknown paths to index.html. Test a deep URL on the deployed site, not just the root - a missing rewrite looks like a working app until someone shares a link.

FAQ

Why is my API key visible in the bundle?
Because everything in the client bundle is public. There is no hidden place on the client, so a secret has to live on your own server and the browser must call that instead - a proxy route or a server action is the usual answer.
How should we ship a fix quickly?
Keep the build reproducible from a commit, deploy the immutable hashed output, and make rollback a matter of pointing the host at the previous artefact. Rebuilding under pressure is when the wrong artefact gets shipped.

Setting up a React project with Vite Testing React components

Last refreshed 2026-09-18.