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| Path | What it is | What to do |
|---|---|---|
dist/index.html | The shell with hashed asset links | Serve it; never edit it by hand |
dist/assets/*.js | Content-hashed chunks | Cache for a year - the name changes with the content |
dist/assets/*.css | Extracted stylesheets | Same caching rule as the scripts |
public/ | Files copied verbatim | Favicons, robots.txt, _redirects |
package-lock.json | Exact dependency versions | Commit it and install with npm ci in CI |
dist/ | The only artefact to deploy | Build 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
})- Build once per commit and treat the output as immutable; the hash in each filename makes that safe to cache.
- Add the SPA rewrite so unknown paths return the shell, and keep static assets resolving before the rewrite applies.
- Check the bundle after adding a dependency: a date library or an icon set can double the entry chunk in one commit.
- Send source maps to the error reporter, not to the public site, so stack traces stay readable without exposing source.
- 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.
Related
Setting up a React project with Vite Testing React components
Last refreshed 2026-09-18.