Steven's Knowledge
Troubleshooting

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 LibraryLighter AlternativeSize 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:

ToolBuild SpeedHMR SpeedBest For
ViteFast (esbuild)InstantNew projects, migration from Webpack
TurbopackVery FastInstantNext.js projects
esbuildFastestManualLibraries, simple apps
SWCVery FastFastDrop-in Babel replacement
RspackVery FastFastWebpack-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 build

Cache 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_)
  • .env files 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 URL

4. 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 assets

Cache 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 declarations

Solution

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 defined

Root 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 ./dist

Summary: Build & Deploy Checklist

IssueSolution
Large bundlesCode splitting, tree shaking, lighter alternatives
Slow buildsVite/Turbopack, incremental TS, CI parallelization
Env var leaksPrefix system, build-time validation, .env.local
Stale cacheContent-hash naming, proper cache headers
Module errorsConsistent path aliases in tsconfig + bundler
SSR failuresGuard browser APIs, dynamic imports
Source mapsHidden source maps, upload to error tracker, delete from deploy

On this page