Course:Internet & Tools/
Lesson

Your app works perfectly with npm run dev. You deploy it, and it breaks. This is one of the most common and frustrating experiences in web development. It happens because development mode and production mode are fundamentally different, they optimize for different goals, use different tools under the hood, and behave differently in subtle ways. AI tools do not understand this distinction, which means they cannot reliably fix "works in dev, broken in prod" issues.

Two modes, two goals

Development mode optimizes for your experience as a developer: fast startup, readable code, helpful error messages. Production mode optimizes for your users: small files, fast downloads, no debug information leaked.

AspectDevelopment (npm run dev)Production (npm run build)
Startup speedInstant (no bundling)Slower (full build pipeline)
File sizesLarge, unminifiedMinified and compressed
Source mapsInline (full detail)Separate file or none
HMRYes, instant updatesNot applicable
Error messagesDetailed with stack tracesMinimal (for security)
FilenamesOriginal (App.tsx)Hashed (App.a1b2c3.js)
Tree shakingNoYes, unused code removed
Code splittingNoYes, lazy-loaded chunks
Build toolesbuild (fast, loose)Rollup (thorough, strict)

The last row is critical: Vite uses different tools for dev and prod. esbuild is fast but less thorough. Rollup is slower but catches more issues. This is why something can work in dev and fail in prod.

02

What happens during npm run dev

When you start the dev server, Vite does not bundleWhat is bundle?A single JavaScript file (or set of files) that a build tool creates by combining all your source code and its imports together. your code. Instead, it intercepts browser requests and transforms files one at a time, on demand:

Browser requests /src/App.tsx
          ↓
Vite intercepts:
  1. Strips TypeScript types
  2. Converts JSX to createElement()
  3. Rewrites import paths to node_modules
          ↓
Browser receives a valid ES module

This is why startup is instant, nothing is processed until the browser actually asks for it. But it also means you are running untested, unoptimized code. Some problems only surface after the full build pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production. runs.

03

What happens during npm run build

A production build runs your code through a complete optimization pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production.:

npm run build
# → dist/
#   ├── index.html
#   ├── assets/
#   │   ├── index.a1b2c3.js      (your app code, minified)
#   │   ├── vendor.d4e5f6.js     (third-party libraries)
#   │   └── index.g7h8i9.css     (all styles, minified)

MinificationWhat is minification?Shrinking JavaScript and CSS files by removing whitespace, shortening variable names, and stripping comments so they download faster. compresses your code:

// Development: 189 bytes, readable
function calculateShippingCost(orderTotal, isExpress) {
  const baseRate = 5.99;
  if (isExpress) {
    return baseRate * 2;
  }
  return orderTotal > 50 ? 0 : baseRate;
}

// Production: 58 bytes (69% smaller)
function s(t,e){const n=5.99;return e?2*n:t>50?0:n}

Tree shakingWhat is tree shaking?A build tool feature that removes unused exported code from your final bundle, reducing the amount of JavaScript shipped to users. removes code you never use:

// You import one function from a utility library
import { debounce } from 'lodash-es';

// Tree shaking includes ONLY debounce's code in the bundle
// The other 300+ lodash functions are excluded entirely

Code splittingWhat is code splitting?Breaking your application into smaller JavaScript chunks that load on demand so users only download the code needed for the page they're viewing. breaks your app into chunks that load on demand:

// This route is lazy-loaded - its code downloads only when
// the user navigates to /settings
const Settings = React.lazy(() => import('./pages/Settings'));

Asset hashingWhat is hashing?A one-way mathematical transformation that turns data (like a password) into a fixed-length string that can't be reversed. Used to store passwords securely. gives each output file a unique name based on its content (main.a1b2c3.js). When you deploy a new version, the hash changes, forcing browsers to download the fresh file instead of serving a stale cached copy.

04

Environment-specific code

Vite provides built-in flags to write mode-specific logic:

// Only runs in development
if (import.meta.env.DEV) {
  console.log('Debug info:', state);
}

// Only runs in production
if (import.meta.env.PROD) {
  analytics.init('prod-key-abc123');
  errorTracking.enable();
}

// Vite removes dead branches at build time:
// In production, the DEV block is completely stripped from the bundle

You can also use different .env files per mode:

.env                  ← loaded in all modes
.env.development      ← loaded only during npm run dev
.env.production       ← loaded only during npm run build
AI pitfall
When you report a production bug to ChatGPT or Claude, they often suggest fixes that work in development but not in production. For example, they might suggest console.log debugging, but those logs are stripped in production builds. Always specify which mode the bug occurs in when asking AI for help, and always test fixes with npm run build && npm run preview.
05

The "works in dev, broken in prod" checklist

This pattern is common enough to deserve a systematic debugging approach:

Problem categorySymptomHow to check
Missing env variablesAPI calls fail, features missingconsole.log(import.meta.env) in dev vs preview
Case-sensitive pathsimport './button' works on Mac, fails on LinuxCheck exact filename capitalization
Hardcoded localhostAPI calls to localhost:3001 fail in prodSearch for localhost in your codebase
Tree shaking side effectsA library's CSS or initialization code disappearsCheck if the import has sideEffects in its package.json
Dynamic importsimport(variable) doesn't work in productionUse import('./known-path.js') with static strings
Missing polyfillsFeature works in Chrome but not Safari/FirefoxCheck browser compatibility on caniuse.com
06

Testing production builds locally

Before deploying, always test the production build on your machine. It takes 30 seconds and catches most "dev vs prod" issues:

# Step 1: Build the production bundle
npm run build

# Step 2: Serve it locally (exactly as a web server would)
npm run preview
# → http://localhost:4173

# Step 3: Test manually
# - Do all pages load?
# - Do API calls work?
# - Do images and styles appear?
# - Does routing work on refresh?

npm run preview is not a development server, it serves your compiled dist/ folder exactly as a static web server would. There is no HMR, no on-demand transforms. What you see is what your users will see.

07

Quick reference

CommandModeOutputUse when
npm run devDevelopmentLive server, HMRWriting code
npm run buildProductiondist/ folderPreparing to deploy
npm run previewProduction (local)Serves dist/Testing before deploy
javascript
// Mode-specific configuration in vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig(({ mode }) => ({
  plugins: [react()],

  build: {
    sourcemap: mode === 'production' ? 'hidden' : true,
    minify: mode === 'production' ? 'esbuild' : false,
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom']
        }
      }
    }
  },

  define: {
    __APP_VERSION__: JSON.stringify(process.env.npm_package_version)
  }
}));