Deploying JavaScript Apps (CDN, Minification, Bundling)

Reviewed & published by Brayan K

Master production deployment: CDNs, bundlers, minification, caching, and everything needed to ship fast, globally optimized JavaScript applications.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn

💡 This lesson covers advanced deployment concepts including build systems (Webpack, Vite), CDN configuration, cache strategies, and production optimization techniques used by major companies.

Why Deployment Is More Than "Upload Files"

Shipping a JavaScript app to the world means getting it to load fast, load globally, be secure, be versioned correctly, use caching properly, and serve as few bytes as possible.

Development Mode

Production Mode

Your goal: Max performance, low bandwidth cost, global speed, zero unnecessary code.

Understanding CDNs (Content Delivery Networks)

A CDN is a global network of servers that stores ("caches") your files and delivers them to users from the closest location. This makes your app load WAY faster globally.

💡 When a user visits your site, they get the file from the nearest CDN node, NOT your origin server. This reduces latency by hundreds of milliseconds.

Popular CDNs:

CDNs host JavaScript bundles, CSS, images, videos, and sometimes API responses.

Minification (Shrinking Your Code)

Minification removes spaces, newlines, long variable names, unused code, comments, and formatting.

// ===== Minification Demo =====
  // Before minification (readable):
  function calculateTotal(price, quantity) {
    const subtotal = price * quantity;
    const tax = subtotal * 0.1;
    return subtotal + tax;
  }

  // Let's test it!
  const total = calculateTotal(100, 5);
  console.log("Price: $100, Quantity: 5");
  console.log("Total with 10% tax: $" + total);

  // After minification (production), this becomes:
  // function calculateTotal(e,t){const n=e*t;return n+.1*n}
  // 
  // Typical size reduction: 30-70% smaller!
  // - Whitespace removed
  // - Variable names shortened (price → e, quantity → t)
  // - Comments stripped
  // - Same functionality, much smaller file!

  console.log("\n✅ Minification keeps the logic, shrinks the file!");

Popular Minification Tools:

Bundling (Combining Files for Fewer Requests)

Browsers load faster when 1 big file loads instead of 30 small ones. Bundlers do tons of work:

Popular Bundlers:

Fastest, uses esbuild for dev and Rollup for production

Most configurable, huge plugin ecosystem

Best for libraries and SDKs

Extremely fast, written in Go

Tree Shaking — Removing Unused Code

Tree shaking automatically removes code you import but never use.

// ===== Tree Shaking Demo =====
  // Imagine we have a math utilities module:

  // math.js exports these functions:
  function add(a, b) { return a + b; }
  function subtract(a, b) { return a - b; }
  function multiply(a, b) { return a * b; }
  function divide(a, b) { return a / b; }

  // In our app, we only USE add():
  const result = add(5, 3);
  console.log("Using add(5, 3):", result);

  // Tree Shaking Analysis:
  console.log("\n📦 What happens in production build:");
  console.log("✓ add() is INCLUDED (we use it)");
  console.log("✗ subtract() is REMOVED (never called)");
  console.log("✗ multiply() is REMOVED (never imported)");
  console.log("✗ divide() is REMOVED (never imported)");

  console.log("\n🎯 Result: Much smaller bundle!");
  console.log("Only the code you actually use ships to production.");

Code Splitting (Huge Performance Boost)

Instead of one giant bundle, create chunks. Only load what the page needs.

Hashing & Cache Busting

Browsers aggressively cache JS files. If you deploy updates but the old file is cached, users see OLD code. Solve this with hashed filenames.

// ===== Hashing & Cache Busting Demo =====

  // Production build creates hashed filenames:
  const buildOutput = {
    files: [
      "app.8c1f19a.js",      // Hash changes when code changes
      "vendor.5a9e02d.js",   // Separate vendor bundle
      "styles.9fc3a44.css"   // CSS also hashed
    ]
  };

  console.log("📁 Production build output:");
  buildOutput.files.forEach(file => console.log("  " + file));

  // When you update your code:
  console.log("\n🔄 After code update:");
  console.log("  OLD: app.8c1f19a.js");
  console.log("  NEW: app.2b4f8e1.js  ← different hash!");

  console.log("\n✅ Benefits:");
  console.log("• Browser sees new filename → downloads fresh version");
  console.log("• Old files can be cached forever (they never change)");
  console.log("• No more 'clear your cache' support tickets!");

  console.log("\n⚠️ Without hashes = broken website updates!");

Compression (Gzip & Brotli)

Browsers support compressed JS files. CDNs automatically compress your bundles.

Older, still common. ~60-70% compression.

Newest, best compression. ~70-80% smaller.

A 600KB file can become 80KB with Brotli. CDNs like Cloudflare and Vercel handle this automatically.

Modern Build Systems

Vite Configuration (Recommended)

// ===== Vite Configuration Reference =====
  // This is what vite.config.js looks like:

  console.log("📄 vite.config.js structure:");
  const viteConfig = {
    build: {
      minify: "terser",       // Use Terser for minification
      sourcemap: true,         // Generate source maps for debugging
      rollupOptions: {
        output: {
          manualChunks: {
            vendor: ["lodash", "axios"]  // Split vendor code
          }
        }
      }
    }
  };

  console.log(JSON.stringify(viteConfig, null, 2));

  console.log("\n✅ This gives you:");
  console.log("  ✔ Minified bundles");
  console.log("  ✔ Source maps for debugging");
  console.log("  ✔ Vendor chunk (cached separately)");
  console.log("  ✔ Super fast build time");

  console.log("\n💡 Run: npm create vite@latest my-app");

Webpack Configuration (Full Control)

// ===== Webpack Configuration Reference =====
  // This is what webpack.prod.js looks like:

  console.log("📄 webpack.prod.js structure:");
  const webpackConfig = {
    mode: "production",
    output: {
      filename: "js/[name].[contenthash].js",
      clean: true
    },
    optimization: {
      minimize: true,
      splitChunks: { chunks: "all" }
    },
    plugins: ["MiniCssExtractPlugin"]
  };

  console.log(JSON.stringify(webpackConfig, null, 2));

  console.log("\n🔧 Key Webpack features:");
  console.log("  ✔ Production mode enables optimizations");
  console.log("  ✔ [contenthash] adds cache-busting hashes");
  console.log("  ✔ clean: true removes old build files");
  console.log("  ✔ splitChunks for automatic code splitting");

  console.log("\n💡 Most modern projects use Vite instead!");

Cache-Control Headers

These headers decide when browser reloads files, what gets cached, and how long it stays cached.

// ===== Cache-Control Headers Demo =====

  console.log("📦 Cache-Control Headers Explained:\n");

  // For hashed JS/CSS files (app.8c1f19a.js):
  console.log("For HASHED files (app.8c1f19a.js):");
  console.log("  Cache-Control: public, max-age=31536000, immutable");
  console.log("  → Cache for 1 YEAR");
  console.log("  → NEVER revalidate");
  console.log("  → Hash guarantees uniqueness");
  console.log("  → INSTANT load for returning visitors!");

  // For HTML files (index.html):
  console.log("\nFor HTML files (index.html):");
  console.log("  Cache-Control: no-cache");
  console.log("  → Always check server for new versions");
  console.log("  → Ensures new deployments load correctly");

  // For API responses:
  console.log("\nFor API responses:");
  console.log("  Cache-Control: max-age=0, must-revalidate");
  console.log("  → Always get fresh data");

  console.log("\n✅ Proper caching = faster loads + less bandwidth!");

Worked Example: Turning a Build Manifest Into Cache Headers

Hashing and caching only pay off together, so here they are in one runnable program. It takes a build manifest — the file list your bundler produces — and decides the right Cache-Control header for every file, then works out what a returning visitor actually has to download.

Read the comments first; every non-obvious line says what it does and why. Then press Run and compare with the expected output at the bottom of the code.

// -- WORKED EXAMPLE - turning a build manifest into cache headers --
  // After "npm run build" you have a folder of files. One of the biggest wins in
  // deployment comes from answering a single question per file: how long may a
  // browser keep this? Get it right and returning visitors download almost nothing.

  // A build manifest - exactly the shape a bundler writes out.
  const manifest = [
    { file: "index.html",                bytes: 1120 },
    { file: "assets/app.8c1f19a.js",     bytes: 184320 },
    { file: "assets/vendor.5a9e02d.js",  bytes: 512400 },
    { file: "assets/styles.9fc3a44.css", bytes: 28900 },
    { file: "assets/logo.svg",           bytes: 4200 },
    { file: "sw.js",                     bytes: 2048 }
  ];

  // A file is "fingerprinted" when its name carries a content hash: a dot, then
  // 6-10 hex characters, then the extension. Change one byte of source and the
  // hash changes, so the URL changes, so the old cache entry is simply never
  // asked for again. That is what makes a one-year cache safe.
  const isFingerprinted = (name) => /\.[0-9a-f]{6,10}\.(js|css)$/.test(name);

  function cacheHeaderFor(name) {
    // index.html is the map to everything else. Cache it and returning users get
    // stuck on an old build pointing at files you have already deleted.
    if (name === "index.html") return "no-cache";

    // A service worker must be re-checked for the same reason.
    if (name === "sw.js") return "no-cache";

    // A fingerprinted file can never change under this name, so a year is safe.
    if (isFingerprinted(name)) return "public, max-age=31536000, immutable";

    // Everything else: keep for an hour, then check back.
    return "public, max-age=3600";
  }

  const kb = (bytes) => (bytes / 1024).toFixed(1) + "kb";

  manifest.forEach(entry => {
    // padEnd/padStart just line the columns up so the table is readable.
    console.log(entry.file.padEnd(28) + kb(entry.bytes).padStart(9) + "   " + cacheHeaderFor(entry.file));
  });

  // Now the number that decides whether your site feels fast to a repeat visitor.
  const firstVisit = manifest.reduce((sum, e) => sum + e.bytes, 0);
  const repeatVisit = manifest
    .filter(e => cacheHeaderFor(e.file) === "no-cache")   // only these come back down
    .reduce((sum, e) => sum + e.bytes, 0);

  console.log("---");
  console.log("first visit downloads:  " + kb(firstVisit));
  console.log("repeat visit (within the hour): " + kb(repeatVisit));

  // Expected output:
  // index.html                      1.1kb   no-cache
  // assets/app.8c1f19a.js         180.0kb   public, max-age=31536000, immutable
  // assets/vendor.5a9e02d.js      500.4kb   public, max-age=31536000, immutable
  // assets/styles.9fc3a44.css      28.2kb   public, max-age=31536000, immutable
  // assets/logo.svg                 4.1kb   public, max-age=3600
  // sw.js                           2.0kb   no-cache
  // ---
  // first visit downloads:  715.8kb
  // repeat visit (within the hour): 3.1kb

Look at the last two lines. The first visit costs 715.8kb; the repeat visit costs 3.1kb. Nothing in the app got smaller — the only change was telling the browser which files are safe to keep. That is the whole argument for content hashing in one number.

🎯 Your Turn — Write the Pre-Deploy Check

Checklists that live in a wiki get skipped. Checklists that run in CI do not. Fill in the three blanks below and you have a script that refuses to let a bad build reach production.

// 🎯 YOUR TURN - write the pre-deploy check
  // Everything is written except the three checks this lesson has been about.

  const build = {
    mode: "production",
    minified: true,
    sourcemapsPublic: true,
    filesHashed: true,
    envVars: { API_URL: "https://api.example.com", API_SECRET: "sk_live_9931" },
    bundleKb: 640
  };

  const problems = [];

  // 👉 1) A production build must be minified. Add the problem when it is NOT.
  //       Replace ___ with a condition that is true for an unminified build.
  if (___) problems.push("bundle is not minified");

  // 👉 2) Public source maps hand your original, unminified code to anyone who
  //       opens devtools. Flag it when build.sourcemapsPublic is true.
  if (___) problems.push("source maps are public");

  // 👉 3) Anything named like a secret must never be baked into a browser bundle.
  //       Flag every env var whose NAME contains the text "SECRET".
  //       Hint: strings have an .includes() method.
  Object.keys(build.envVars).forEach(key => {
    if (___) problems.push("secret in client bundle: " + key);
  });

  // (these two are written for you)
  if (!build.filesHashed) problems.push("files are not content-hashed");
  if (build.bundleKb > 500) problems.push("bundle over budget: " + build.bundleKb + "kb");

  if (problems.length === 0) {
    console.log("READY to deploy");
  } else {
    console.log("STOP - " + problems.length + " problem(s), do not deploy:");
    problems.forEach(p => console.log("   - " + p));
  }

  // ✅ Expected output:
  // STOP - 3 problem(s), do not deploy:
  //    - source maps are public
  //    - secret in client bundle: API_SECRET
  //    - bundle over budget: 640kb

Blank 3 is the one that matters most in real life. A bundler will happily inline API_SECRET into your JavaScript, and it is then readable by anyone who views source — no hacking required. Secrets belong on the server, never in a client bundle.

Automated Deployment (CI/CD)

Push to GitHub → CI builds your JS → CI uploads to CDN → deployment is fully automatic.

// ===== CI/CD Pipeline Demo =====
  // GitHub Actions automatically builds and deploys your app!

  console.log("🔄 CI/CD Pipeline Steps:\n");

  const pipelineSteps = [
    "1. Developer pushes code to 'main' branch",
    "2. GitHub Actions triggers workflow",
    "3. Checkout code from repository",
    "4. Setup Node.js environment",
    "5. Run: npm install",
    "6. Run: npm run build (creates optimized bundle)",
    "7. Deploy to CDN (Cloudflare, Vercel, etc.)",
    "8. App is live globally in seconds!"
  ];

  pipelineSteps.forEach(step => console.log(step));

  console.log("\n📄 This is defined in .github/workflows/deploy.yml");
  console.log("\n✅ Benefits:");
  console.log("  • No manual uploads");
  console.log("  • Consistent builds every time");
  console.log("  • Automatic on every push");
  console.log("  • Rollback by reverting commits");

SPA Rewrite Rules (Critical!)

For history mode routing, all routes must serve index.html. Without this rule, refreshing SPA pages will crash.

HTTP Security Headers

Large companies REQUIRE these headers for production apps.

🏆 Mini-Challenge: Upload Only What Changed

No blanks this time — a brief, an outline and a test harness. This is the last piece of the hashing story: because a changed file gets a new name, working out what to upload is a filename comparison rather than a byte-by-byte diff.

// 🎯 MINI-CHALLENGE: work out what actually needs uploading
  //
  // Brief: deploys are faster and far less risky when you upload only what
  // changed. Because filenames carry a content hash, "changed" is just a
  // filename comparison - no diffing of file contents required.
  //
  // Write diffManifests(oldFiles, newFiles) returning:
  //   { added: [...], removed: [...], unchanged: [...] }
  //
  // Outline - no logic filled in, that part is yours:
  //   added     -> names in newFiles that are not in oldFiles   (upload these)
  //   removed   -> names in oldFiles that are not in newFiles   (delete these)
  //   unchanged -> names in newFiles that are also in oldFiles   (leave alone)
  //   Hint: Array has .filter() and .includes().
  //
  // Until you have written it, running this throws
  // "ReferenceError: diffManifests is not defined" - that is expected, not a bug.

  // your code here


  // --- test harness: do not change anything below this line ---
  const before = [
    "index.html",
    "assets/app.8c1f19a.js",
    "assets/vendor.5a9e02d.js",
    "assets/old-feature.11aa22b.js"
  ];
  const after = [
    "index.html",
    "assets/app.2b4f8e1.js",
    "assets/vendor.5a9e02d.js",
    "assets/new-feature.77cc88d.js"
  ];

  const d = diffManifests(before, after);
  console.log("upload:", d.added.join(", "));
  console.log("delete:", d.removed.join(", "));
  console.log("keep:  ", d.unchanged.join(", "));
  console.log("uploaded " + d.added.length + " of " + after.length + " files");

  // ✅ Expected output:
  // upload: assets/app.2b4f8e1.js, assets/new-feature.77cc88d.js
  // delete: assets/app.8c1f19a.js, assets/old-feature.11aa22b.js
  // keep:   index.html, assets/vendor.5a9e02d.js
  // uploaded 2 of 4 files

One detail worth thinking about: delete the removed files after the new ones are live, never before. A visitor who loaded the old index.html a second ago is still asking for the old chunks, and deleting them first gives them a broken page.

The Ultimate Production Checklist

Performance

Security

Stability

Deployment

Key Takeaways

Practice quiz

What is a CDN (Content Delivery Network)?

  • A tool that minifies JavaScript
  • A type of bundler
  • A global network of servers that caches files and serves them from the closest location
  • A database for storing user data

Answer: A global network of servers that caches files and serves them from the closest location. A CDN is a global network of servers that caches your files and delivers them from the node nearest the user, reducing latency.

What does minification do to your code?

  • Removes whitespace, comments, and shortens variable names while keeping the logic
  • Encrypts it for security
  • Splits it into multiple files
  • Converts it to TypeScript

Answer: Removes whitespace, comments, and shortens variable names while keeping the logic. Minification removes spaces, newlines, comments, and shortens names, typically reducing file size 30-70% while keeping functionality.

What does tree shaking remove?

  • Duplicate CSS rules
  • Source maps
  • Compressed assets
  • Code you import but never use

Answer: Code you import but never use. Tree shaking automatically removes code you import but never use, so only the code you actually use ships to production.

Why do production builds use hashed filenames like app.8c1f19a.js?

  • To hide the source code
  • For cache busting, so updated code gets a new filename and isn't served stale
  • To make files smaller
  • To enable HTTPS

Answer: For cache busting, so updated code gets a new filename and isn't served stale. When code changes the hash changes, so the browser downloads the fresh version while old, unchanged files can be cached forever.

Which compression format gives the best compression (~70-80% smaller)?

  • Brotli
  • Gzip
  • Zip
  • Deflate

Answer: Brotli. Brotli is the newest format with the best compression (~70-80% smaller); Gzip is older at ~60-70%.

What Cache-Control header is recommended for hashed JS/CSS files?

  • no-cache
  • max-age=0, must-revalidate
  • public, max-age=31536000, immutable
  • private, no-store

Answer: public, max-age=31536000, immutable. Hashed files use 'public, max-age=31536000, immutable' to cache for a year and never revalidate, since the hash guarantees uniqueness.

Why is an SPA rewrite rule (serve index.html for all routes) critical?

  • It speeds up the CDN
  • Without it, refreshing a route like /dashboard returns a 404
  • It minifies the HTML
  • It enables tree shaking

Answer: Without it, refreshing a route like /dashboard returns a 404. For history-mode routing, all routes must serve index.html; without the rewrite rule, refreshing an SPA page crashes with a 404.

What does the Content-Security-Policy header primarily protect against?

  • Clickjacking
  • MIME sniffing
  • Slow load times
  • XSS attacks

Answer: XSS attacks. Content-Security-Policy prevents XSS attacks; X-Frame-Options handles clickjacking and X-Content-Type-Options handles MIME sniffing.

In a CI/CD pipeline, what triggers the automated build and deploy?

  • Manually uploading files via FTP
  • Pushing code to the main branch
  • Refreshing the browser
  • Clearing the CDN cache

Answer: Pushing code to the main branch. Pushing to main triggers GitHub Actions to checkout, install, build, and deploy to the CDN automatically.

Which header forces browsers to always use HTTPS?

  • X-Frame-Options
  • X-Content-Type-Options
  • Strict-Transport-Security
  • Cache-Control

Answer: Strict-Transport-Security. Strict-Transport-Security (e.g. max-age=31536000; includeSubDomains) forces HTTPS for the site.

Continue this course