React 100 ⚛️ Production Builds, Bundling, and Deployment Strategies
Production builds are not a single command but a sequence of decisions: which bundler processes the source, which environment variables are embedded at build time, and where the resulting artifacts are served from. React’s development build includes warnings and debugging tools that are intentionally removed in production; the production build is typically two to three times faster . The deployment strategy determines whether the application is served as static files from a CDN, rendered on a server per request, or packaged as a container. Each choice has consequences for cache invalidation, environment configuration, and the routing behavior of single-page applications.
This chapter covers the bundler choice between Vite and Webpack, the build-time environment variable model that determines what is public and what is private, and the deployment targets that determine how the production artifacts are served. It examines the SPA rewrite rule that separates a working deployment from a broken one, and the content-hashing mechanism that makes cache invalidation automatic.
Key point: Environment variables in React are embedded into the bundle at build time, not read at runtime. A value that changes between deployments requires a rebuild, not a restart. This is why secrets must never be placed in variables prefixed for client-side exposure.
Why production deployment requires deliberate configuration
The development artifact problem. A React application served from the development server includes hot module replacement, unminified source, and development-only warnings. Deploying this artifact to production delivers a slower application and exposes implementation details. The production build pipeline minifies code, removes dead branches, and strips development warnings . The bundler that produces this artifact is a separate decision from the deployment target.
The SPA routing problem. A React single-page application handles navigation in the browser through a client-side router. When a user refreshes the page at /dashboard, the server receives a request for a path that does not correspond to a file. Without a rewrite rule that serves index.html for unknown paths, the CDN or static host returns a 404 . This is a deployment configuration issue, not a React issue, and it is the most common failure when first deploying a React SPA.
The environment variable problem. React does not have runtime environment variables in the way a server-side application does. Variables prefixed with REACT_APP_ (Create React App) or VITE_ (Vite) are replaced with their literal values during the build . A staging build and a production build of the same source produce different bundles. The deployment pipeline must supply the correct values before the build runs, and the values become visible to anyone who inspects the JavaScript.
The bundler choice problem. Vite and Webpack approach bundling differently. Vite uses native ES modules during development, which makes the development server start faster and the feedback loop shorter. Webpack requires explicit configuration for loaders and code splitting, which provides more control but more maintenance . The choice affects the build configuration, the plugin ecosystem, and the error messages a developer sees when something fails.
a. Bundling with Vite or Webpack
Vite is the default recommendation for new React projects with modern dependencies. It requires minimal configuration: a React plugin and a defineConfig call are sufficient for most applications . Code splitting is handled automatically through dynamic imports, and the development server reflects changes without a full rebuild.
// vite.config.js — minimal Vite configuration for React
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
// Base path if the app is served from a subdirectory
base: '/',
});
Webpack remains appropriate when the build requires custom loaders, unusual asset handling, or integration with an existing toolchain that expects Webpack’s plugin API. The configuration is more verbose, but the control is more granular . The decision is explicit: new apps with modern dependencies favor Vite; large apps with highly customized build steps favor Webpack.
// webpack.config.js — React configuration with Babel and CSS
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[contenthash].js',
},
module: {
rules: [
{ test: /\.jsx?$/, exclude: /node_modules/, use: 'babel-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
],
},
};
The output filename includes a content hash. This is the mechanism for cache invalidation: when the file content changes, the hash changes, and the browser fetches the new file instead of serving a stale cached copy. Vite includes this by default; Webpack requires it to be configured.
b. Environment Variables at Build Time
Create React App exposes variables prefixed with REACT_APP_; Vite exposes variables prefixed with VITE_ . The prefix is not a naming convention; it is the mechanism that determines whether a variable is replaced in the client bundle. A variable without the prefix is available to the build process but not to the application code.
# .env.production — values embedded into the production bundle
REACT_APP_API_URL=https://api.example.com
REACT_APP_ENVIRONMENT=production
# Vite equivalent
VITE_API_URL=https://api.example.com
// Accessing the embedded value in application code
const apiUrl = process.env.REACT_APP_API_URL;
// or, with Vite:
const apiUrl = import.meta.env.VITE_API_URL;
The critical rule is that these values are public. Anyone who downloads the bundle can read them. API keys, database credentials, and signing secrets must not be placed in prefixed variables. The correct pattern is a backend proxy or a serverless function that holds the secret and forwards requests .
For Next.js, the distinction is between build-time and runtime variables. Variables prefixed with NEXT_PUBLIC_ are embedded into the client bundle. Variables without the prefix are available only in server-side code, such as API routes and getServerSideProps . This allows secrets to remain on the server while public configuration is embedded.
c. Deployment Targets and Routing Configuration
The deployment target determines how the production artifacts are served. A static site host—S3 with CloudFront, Netlify, Cloudflare Pages, GitHub Pages—serves the files from a CDN and requires no server. A platform like Vercel or Netlify can also run server-side rendering or serverless functions. A container deployment runs a Node.js process that serves both the static assets and the server-rendered responses.
For a static React SPA, the rewrite rule is mandatory. AWS Amplify handles this with a rule that routes requests for non-file paths to index.html . Netlify uses a _redirects file or a netlify.toml configuration. Vercel uses a vercel.json rewrite. The syntax differs, but the principle is identical: any path that is not a static asset must resolve to the SPA entry point.
# amplify.yml — build specification for a Vite React app
version: 1
frontend:
phases:
preBuild:
commands:
- npm ci
build:
commands:
- npm run build
artifacts:
baseDirectory: dist
files:
- '**/*'
The amplify.yml file is committed to the repository, which means the build configuration is versioned alongside the code. The deployment platform detects the file and uses it instead of a dashboard-configured build command .
For a Next.js application with server-side rendering, the deployment target must support a Node.js runtime. Netlify’s OpenNext adapter provisions a serverless function for SSR, ISR, and API routes, and configures caching for the Next.js route cache . A container deployment runs next start or, with standalone output, a minimal server that includes only the required files .
Complete Example Session
# ============================================
# PART 1: CREATE A VITE REACT APPLICATION
# ============================================
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
# ============================================
# PART 2: CONFIGURE ENVIRONMENT VARIABLES
# ============================================
# .env.production — embedded at build time
VITE_API_URL=https://api.example.com
VITE_ENVIRONMENT=production
# .env.development — used by the dev server
VITE_API_URL=http://localhost:3000
# ============================================
# PART 3: PRODUCE THE PRODUCTION BUILD
# ============================================
# Minifies code, removes dev warnings
npm run build
# Output: dist/ directory with index.html and hashed assets
# ============================================
# PART 4: VERIFY THE BUILD OUTPUT
# ============================================
ls dist/
# index.html, assets/index-[hash].js, assets/index-[hash].css
# The hash changes when content changes
# ============================================
# PART 5: CONFIGURE SPA ROUTING ON AWS AMPLIFY
# ============================================
# amplify.yml — committed to repository root
version: 1
frontend:
phases:
preBuild:
commands: [npm ci]
build:
commands: [npm run build]
artifacts:
baseDirectory: dist
files: ['**/*']
# ============================================
# PART 6: ADD REWRITE RULE FOR CLIENT ROUTES
# ============================================
# In Amplify console: Hosting → Rewrites and redirects
# Source: </^[^.]+$|\.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|woff2|ttf|map|json)$)([^.]+$)/>
# Target: /index.html
# Type: 200 (Rewrite)
# ============================================
# PART 7: CONNECT REPOSITORY TO AMPLIFY
# ============================================
# Push to GitHub, connect in Amplify console
# Amplify detects amplify.yml and builds automatically
# ============================================
# PART 8: DEPLOY A NEXT.JS APP WITH SSR
# ============================================
# Next.js requires a Node.js runtime for SSR
# Netlify's OpenNext adapter provisions the function
# npm run build && npx opennextjs-netlify build
# ============================================
# PART 9: CONFIGURE SECRETS ON THE SERVER
# ============================================
# NEXT_PUBLIC_* variables are embedded (public)
# Non-prefixed variables remain server-only
# DATABASE_URL=postgresql://... (not exposed)
# NEXT_PUBLIC_API_URL=https://... (exposed)
# ============================================
# PART 10: VERIFY DEPLOYMENT
# ============================================
# Visit the production URL
# Navigate to /dashboard and refresh
# If 404, the rewrite rule is missing or incorrect
# If page loads, SPA routing is configured correctly
The ten parts trace the path from project creation through environment configuration, build, deployment, and verification. The rewrite rule in Part 6 is the detail that separates a working SPA deployment from a broken one.
Quick Reference
Bundler Comparison
| Feature | Vite | Webpack |
|---|---|---|
| Configuration | Minimal | Explicit, verbose |
| Dev server speed | Fast (native ESM) | Slower (bundles first) |
| Code splitting | Automatic | Configured |
| Plugin ecosystem | Growing | Mature |
| Best for | New projects, modern deps | Custom build steps |
Environment Variable Prefixes
| Tool | Prefix | Client Exposure |
|---|---|---|
| Create React App | REACT_APP_ | Yes |
| Vite | VITE_ | Yes |
| Next.js (client) | NEXT_PUBLIC_ | Yes |
| Next.js (server) | (none) | No |
Deployment Targets
| Target | SSR Support | Routing Config |
|---|---|---|
| Static S3/CloudFront | No | CloudFront function or S3 redirect |
| AWS Amplify | Yes | amplify.yml + rewrite rules |
| Netlify | Yes (Next.js) | _redirects or netlify.toml |
| Vercel | Yes (Next.js) | vercel.json |
| Container | Yes | Application-level routing |
Build Commands
| Tool | Command | Output |
|---|---|---|
| Vite | npm run build | dist/ |
| Create React App | npm run build | build/ |
| Next.js (static) | next build (export) | out/ |
| Next.js (SSR) | next build | .next/ |
Best Practices
✅ Do This:
# Use production build for deployment
npm run build # ✅
# Prefix client-exposed variables correctly
VITE_API_URL=https://api.example.com # ✅
# Commit build specification to repository
# amplify.yml, netlify.toml, vercel.json # ✅
# Configure SPA rewrite for client-side routes
# Unknown paths → /index.html # ✅
# Use content-hashed filenames for cache invalidation
bundle.[contenthash].js # ✅
❌ Don’t Do This:
# Deploy development build to production
npm run dev # not for production # ❌
# Put secrets in client-exposed variables
REACT_APP_SECRET=sk_live_... # ❌
# Omit SPA rewrite rule
# Refreshing /dashboard returns 404 # ❌
# Hardcode environment-specific URLs in source
fetch('https://api.example.com/data') # ❌
# Use Vite for a project that requires Webpack-specific loaders
# Migration cost may exceed the benefit # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| 404 on page refresh | No SPA rewrite rule | Configure rewrite to index.html |
| Environment variable is undefined | Missing prefix or wrong .env file | Check prefix and build environment |
| Stale bundle served after deploy | Filename not hashed | Configure content hashing |
| Build fails on CI | Missing environment variables | Set variables in CI pipeline |
| Next.js SSR fails on static host | No Node.js runtime | Use a platform with server support |
Real-World Examples
1. Vite Production Build
npm run build
# Output: dist/
2. Webpack Content Hash
output: { filename: 'bundle.[contenthash].js' }
3. Create React App Environment Variable
REACT_APP_API_URL=https://api.example.com
4. Vite Environment Variable
VITE_API_URL=https://api.example.com
5. AWS Amplify Build Spec
version: 1
frontend:
phases:
build:
commands: [npm run build]
artifacts:
baseDirectory: dist
6. SPA Rewrite Rule (Amplify)
Source: </^[^.]+$|\.(?!(css|gif|ico|jpg|js|png)$)([^.]+$)/>
Target: /index.html
Type: 200
7. Next.js Server-Only Variable
DATABASE_URL=postgresql://...
# Not prefixed, not exposed to client
8. Next.js Public Variable
NEXT_PUBLIC_API_URL=https://api.example.com
# Prefixed, embedded in client bundle
9. Netlify Next.js Deployment
npx opennextjs-netlify build
# Provisions serverless function for SSR
10. Container Deployment
node .next/standalone/server.js
# Standalone Next.js output
Visual
Build and Deploy Pipeline
┌──────────────────────────────────────────────────────────────┐
│ SOURCE CODE │
│ ├── React components │
│ ├── .env.production │
│ └── vite.config.js / webpack.config.js │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ BUNDLER │ Vite or Webpack │
│ │ npm run build │ Minify, tree-shake, hash filenames │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ ARTIFACTS │ dist/ or build/ │
│ │ index.html │ Environment values embedded │
│ │ assets/*.js │ Development warnings removed │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ DEPLOY │ CDN, static host, or server │
│ │ │ Rewrite rules for SPA routing │
│ └─────────────────┘ │
│ │
│ Build-time configuration is fixed at build. │
│ Runtime configuration requires a server. │
│ │
└──────────────────────────────────────────────────────────────┘
SPA Routing Problem
┌──────────────────────────────────────────────────────────────┐
│ USER VISITS /dashboard AND REFRESHES │
│ │
│ ┌─────────────────┐ │
│ │ CDN receives │ │
│ │ request for │ │
│ │ /dashboard │ │
│ └────────┬────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ ▼ ▼ │
│ ┌──────┐ ┌──────┐ │
│ │ File │ │ No │ │
│ │exists│ │ file │ │
│ └──┬───┘ └──┬───┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────┐ ┌─────────────────┐ │
│ │ 200 │ │ 404 NOT FOUND │ │
│ │ Serve│ │ Without rewrite │ │
│ └──────┘ └─────────────────┘ │
│ │
│ FIX: Rewrite all non-file paths to /index.html │
│ The SPA router then handles the path client-side. │
│ │
└──────────────────────────────────────────────────────────────┘
Environment Variable Scope
┌──────────────────────────────────────────────────────────────┐
│ BUILD TIME RUNTIME │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ REACT_APP_* │ │ Server process │ │
│ │ VITE_* │ │ reads env at │ │
│ │ NEXT_PUBLIC_* │ │ startup │ │
│ │ │ │ │ │
│ │ Embedded in │ │ Next.js API │ │
│ │ bundle at build │ │ routes, SSR │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ PUBLIC │ │ PRIVATE │ │
│ │ Visible in │ │ Never sent to │ │
│ │ browser bundle │ │ client │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ Client-exposed variables are not secrets. │
│ Secrets belong in server-side configuration. │
│ │
└──────────────────────────────────────────────────────────────┘
Vite vs Webpack Decision
┌──────────────────────────────────────────────────────────────┐
│ NEW PROJECT? │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Modern deps │ │ Custom loaders │ │
│ │ Standard setup │ │ Unusual assets │ │
│ │ Fast feedback │ │ Existing infra │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ VITE │ │ WEBPACK │ │
│ │ │ │ │ │
│ │ Minimal config │ │ Explicit config │ │
│ │ Fast dev server │ │ Mature plugins │ │
│ │ ESM native │ │ Deep control │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ Both produce production bundles. │
│ The choice is about development workflow and build control. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Production build | Minified, tree-shaken, dev warnings removed |
| Vite default | Minimal config, fast dev server, automatic splitting |
| Webpack default | Explicit config, mature plugins, deep control |
| Client env prefix (CRA) | REACT_APP_ |
| Client env prefix (Vite) | VITE_ |
| Client env prefix (Next.js) | NEXT_PUBLIC_ |
| Env timing | Embedded at build, not runtime |
| SPA routing | Rewrite unknown paths to index.html |
| Cache invalidation | Content-hashed filenames |
| Build spec file | amplify.yml, netlify.toml, vercel.json |
Key takeaways:
- The production build is not the development build. Development mode includes warnings and debugging tools that make it slower. The production build removes these and minifies the output, typically making React two to three times faster .
- Vite is the default for new React projects; Webpack remains for custom build requirements. Vite’s minimal configuration and fast development server suit modern projects. Webpack’s explicit configuration and mature plugin ecosystem suit projects with unusual asset handling or existing toolchain integration .
- Environment variables are embedded at build time. The prefix (
REACT_APP_,VITE_,NEXT_PUBLIC_) determines whether a value is exposed to the client. Prefixed values are public and must never contain secrets . Server-only variables in Next.js remain on the server. - SPA routing requires a rewrite rule. Client-side navigation works without configuration, but refreshing a subpath sends a request for a file that does not exist. The host must rewrite unknown paths to
index.html. - Content hashing is the cache invalidation mechanism. A filename that includes a hash of the content changes when the content changes, forcing the browser to fetch the new version instead of serving a stale cached copy.
- The deployment target determines the runtime. A static host serves files from a CDN and requires no server. A platform with SSR support runs a Node.js process per request. The build output and the deployment target must match.
Remember: Production deployment is the point where the decisions made during development become visible to users. The bundler determines the shape of the artifact. The environment variable prefix determines what is public and what is private. The rewrite rule determines whether a page refresh works. The deployment target determines whether server-side rendering is possible. None of these decisions is reversible at runtime; they are all fixed at build time or deployment configuration. The build command is the same in development and production, but the environment, the configuration, and the target are different. Deploying the development build, exposing a secret in a prefixed variable, or omitting the SPA rewrite rule are the three failures that account for most first-deployment problems. Each has a specific fix, and each fix belongs in the build configuration or the host configuration, not in the React code.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!