Build & Deploy
Build failures, bundle size problems, and deployment issues in frontend projects
Build & Deploy Troubleshooting
Build and deployment issues can block entire teams. This guide covers common problems from local development builds to production deployment failures.
1. Bundle Size Too Large — Slow Page Load
Problem
- Initial page load takes several seconds
- Lighthouse performance score is low
- Users on slow networks cannot use the app
- JavaScript payload exceeds 200KB+ gzipped
Diagnosis
# Webpack bundle analyzer
npx webpack-bundle-analyzer stats.json
# Vite — visualize bundle
npx vite-bundle-visualizer
# Next.js — built-in analysis
ANALYZE=true next build
# Check package size before installing
npx bundlephobia <package-name>Solution
Code splitting with dynamic imports:
// Route-level code splitting
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}Tree shaking — use ES modules:
// ✗ Bad — imports entire library
import _ from 'lodash';
_.pick(obj, ['a', 'b']);
// ✓ Good — tree-shakeable
import pick from 'lodash/pick';
pick(obj, ['a', 'b']);
// ✓ Best — use native methods
const { a, b } = obj;Replace heavy dependencies:
| Heavy Library | Lighter Alternative | Size Reduction |
|---|---|---|
| moment.js (330KB) | date-fns (tree-shakeable), dayjs (2KB) | ~95% |
| lodash (72KB) | lodash-es (tree-shakeable), native JS | ~80% |
| numeral (32KB) | Intl.NumberFormat (native) | 100% |
| uuid (12KB) | crypto.randomUUID() (native) | 100% |
| axios (29KB) | Native fetch + wrapper | ~90% |
Compression and optimization:
// vite.config.ts
import { defineConfig } from 'vite';
import { compression } from 'vite-plugin-compression2';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
},
},
},
target: 'es2020',
minify: 'terser',
},
plugins: [
compression({ algorithm: 'gzip' }),
compression({ algorithm: 'brotliCompress' }),
],
});2. Slow Build Times
Problem
- Build takes minutes, slowing development
- CI/CD pipeline is bottlenecked by build step
- Hot Module Replacement (HMR) is slow
Solution
Migrate to faster build tools:
| Tool | Build Speed | HMR Speed | Best For |
|---|---|---|---|
| Vite | Fast (esbuild) | Instant | New projects, migration from Webpack |
| Turbopack | Very Fast | Instant | Next.js projects |
| esbuild | Fastest | Manual | Libraries, simple apps |
| SWC | Very Fast | Fast | Drop-in Babel replacement |
| Rspack | Very Fast | Fast | Webpack-compatible migration |
Optimize TypeScript compilation:
// tsconfig.json
{
"compilerOptions": {
"incremental": true, // Incremental compilation
"tsBuildInfoFile": ".tsbuildinfo", // Cache build info
"skipLibCheck": true, // Skip checking node_modules types
"isolatedModules": true // Required for SWC/esbuild
}
}Parallelize CI builds:
# GitHub Actions — parallel jobs
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- run: npx tsc --noEmit
lint:
runs-on: ubuntu-latest
steps:
- run: npx eslint .
test:
runs-on: ubuntu-latest
steps:
- run: npx vitest run
build:
needs: [typecheck, lint, test]
runs-on: ubuntu-latest
steps:
- run: npm run buildCache dependencies in CI:
- uses: actions/cache@v4
with:
path: |
node_modules
.next/cache
~/.cache/turbo
key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-modules-3. Environment Variable Issues
Problem
- Environment variables undefined in production
- Secrets exposed in client-side bundle
- Different behavior between local and production
Root Cause
- Build-time vs runtime environment variables confusion
- Missing env var prefixes (
NEXT_PUBLIC_,VITE_) .envfiles not loaded or in wrong order
Solution
Understand the env var lifecycle:
# .env — all environments (committed, non-secret values only)
NEXT_PUBLIC_APP_NAME=MyApp
# .env.local — local overrides (gitignored)
NEXT_PUBLIC_API_URL=http://localhost:3000
# .env.production — production values
NEXT_PUBLIC_API_URL=https://api.production.com
# Server-only secrets — NEVER prefix with NEXT_PUBLIC_ or VITE_
DATABASE_URL=postgresql://...
API_SECRET_KEY=sk-...Validate env vars at build time with @t3-oss/env-nextjs:
// src/env.ts
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
API_SECRET: z.string().min(1),
},
client: {
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_APP_NAME: z.string(),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
API_SECRET: process.env.API_SECRET,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
},
});
// Usage — fully type-safe, validates at startup
import { env } from '@/env';
console.log(env.NEXT_PUBLIC_API_URL); // string, guaranteed to be a valid URL4. Deployment Failures — Cache and Versioning Issues
Problem
- Users see old version after deployment
- Static assets return 404 after deploy
- CSS/JS mismatch between cached HTML and new assets
- Service Worker serves stale content
Solution
Content-hash file naming (default in modern bundlers):
dist/
assets/
main.a1b2c3d4.js # Hash changes when content changes
style.e5f6g7h8.css
index.html # Always fresh — references hashed assetsCache headers strategy:
# nginx configuration
# HTML — never cache (always fetch latest)
location / {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# Hashed assets — cache forever (hash changes = new URL)
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}Service Worker update strategy:
// In service worker registration
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.register('/sw.js');
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker?.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
// Prompt user to refresh for new version
if (confirm('New version available. Reload?')) {
window.location.reload();
}
}
});
});
}5. Module Resolution Errors
Problem
Module not found: Can't resolve './Component'
Cannot find module '@/utils/helpers' or its corresponding type declarationsSolution
Configure path aliases consistently:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@utils/*": ["./src/utils/*"]
}
}
}// vite.config.ts
import { resolve } from 'path';
export default defineConfig({
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'@components': resolve(__dirname, './src/components'),
'@utils': resolve(__dirname, './src/utils'),
},
},
});File extension resolution:
// vite.config.ts
export default defineConfig({
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
},
});6. SSR/SSG Build Failures
Problem
ReferenceError: window is not defined
ReferenceError: document is not defined
ReferenceError: localStorage is not definedRoot Cause
Server-side rendering executes code in Node.js where browser APIs don't exist.
Solution
Guard browser API access:
// utils/env.ts
export const isBrowser = typeof window !== 'undefined';
// Usage
function getStoredTheme() {
if (!isBrowser) return 'light'; // Default for SSR
return localStorage.getItem('theme') ?? 'light';
}Dynamic imports for browser-only libraries:
// Next.js
import dynamic from 'next/dynamic';
const MapComponent = dynamic(() => import('react-leaflet').then(mod => mod.MapContainer), {
ssr: false,
loading: () => <div style={{ height: 400 }}>Loading map...</div>,
});Conditional hooks pattern:
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia(query);
setMatches(mediaQuery.matches);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
return matches;
}7. Source Map Issues in Production
Problem
- Stack traces show minified code — impossible to debug
- Source maps accidentally exposed in production (security risk)
- Source maps not uploaded to error tracking service
Solution
Generate source maps for error tracking only:
// vite.config.ts
export default defineConfig({
build: {
sourcemap: 'hidden', // Generate but don't reference in bundle
},
});Upload source maps to error tracking, then delete:
# CI/CD pipeline
- name: Build
run: npm run build
- name: Upload source maps to Sentry
run: npx @sentry/cli sourcemaps upload --release=${{ github.sha }} ./dist
- name: Remove source maps from deployment
run: find ./dist -name '*.map' -delete
- name: Deploy
run: deploy ./distSummary: Build & Deploy Checklist
| Issue | Solution |
|---|---|
| Large bundles | Code splitting, tree shaking, lighter alternatives |
| Slow builds | Vite/Turbopack, incremental TS, CI parallelization |
| Env var leaks | Prefix system, build-time validation, .env.local |
| Stale cache | Content-hash naming, proper cache headers |
| Module errors | Consistent path aliases in tsconfig + bundler |
| SSR failures | Guard browser APIs, dynamic imports |
| Source maps | Hidden source maps, upload to error tracker, delete from deploy |