Module Bundlers: Webpack, Parcel & Vite
Reviewed & published by Brayan K
Master the tools that power modern JavaScript development. Learn bundling, code splitting, tree shaking, HMR, and production optimization across Webpack, Vite, and Parcel.
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.
Bundler configurations require a local development environment. To practice:
- Install Node.js on your computer
- Create a new project folder and run npm init -y
- Try npm create vite@latest for the easiest start
What You'll Learn
- Webpack configuration
- Vite for fast development
- Code splitting & lazy loading
- Tree shaking dead code
- Hot Module Replacement
- Production optimization
Why Module Bundlers?
Modern apps use ES modules, TypeScript, JSX, SCSS, and hundreds of dependencies. Browsers can't handle all of this natively. Bundlers transform your development code into optimized, browser-ready production builds.
What Bundlers Do
- ✓ Combine modules into bundles
- ✓ Transform TS/JSX → JavaScript
- ✓ Minify and compress code
- ✓ Remove unused code (tree shaking)
- ✓ Handle images, fonts, CSS
- ✓ Enable hot module replacement
The Big Three
- Webpack — Most powerful & configurable
- Vite — Fastest dev experience (modern default)
- Parcel — Zero configuration needed
Before you press Run on the config blocks: most of the code in this lesson is configuration — webpack.config.js, vite.config.js and friends. Those files are read by a bundler on your machine, not by a browser, so running them in the editor here reports errors like require is not defined or Cannot use import statement outside a module. That is expected. Read them, copy them into a real project, and run npm run build there.
The three exercises further down are runnable, because they model what a bundler does using plain JavaScript you can execute right here.
Webpack Configuration
Webpack is the most flexible bundler with a massive plugin ecosystem. It requires configuration but gives you complete control over every aspect of your build.
// webpack.config.js - Basic Webpack Configuration
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
// Entry point - where bundler starts
entry: "./src/index.js",
// Output configuration
output: {
filename: "[name].[contenthash].js",
path: path.resolve(__dirname, "dist"),
clean: true // Clean /dist folder before each build
},
// Development or production mode
mode: process.env.NODE_ENV || "development",
// Module rules - how to handle different file types
module: {
rules: [
// JavaScript/JSX with Babel
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"]
}
}
},
// CSS files
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
},
// Images
{
test: /\.(png|jpg|gif|svg)$/,
type: "asset/resource"
}
]
},
// Plugins extend Webpack functionality
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html"
}),
new MiniCssExtractPlugin({
filename: "[name].[contenthash].css"
})
],
// Development server
devServer: {
static: "./dist",
hot: true,
port: 3000
},
// Code splitting optimization
optimization: {
splitChunks: {
chunks: "all"
},
runtimeChunk: "single"
}
};💡 Key Concepts: Entry points define where bundling starts. Loaders transform files (babel-loader, css-loader). Plugins extend functionality (HtmlWebpackPlugin, MiniCssExtractPlugin).
Webpack Production Optimization
Production builds require aggressive optimization: minification, code splitting, tree shaking, and vendor chunking for optimal caching.
// webpack.config.js - Production Optimization
const TerserPlugin = require("terser-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
module.exports = {
mode: "production",
// Multiple entry points for large apps
entry: {
main: "./src/main.js",
admin: "./src/admin.js",
analytics: "./src/analytics.js"
},
output: {
filename: "[name].[contenthash].js",
path: __dirname + "/dist",
// Enable long-term caching
chunkFilename: "[name].[contenthash].chunk.js"
},
optimization: {
minimize: true,
minimizer: [
// Minify JavaScript
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // Remove console.log
pure_funcs: ["console.info", "console.debug"]
}
}
}),
// Minify CSS
new CssMinimizerPlugin()
],
// Split vendor code for better caching
splitChunks: {
chunks: "all",
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all"
},
// Separate React into its own chunk
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: "react",
chunks: "all",
priority: 10
}
}
},
// Single runtime chunk for all entries
runtimeChunk: "single"
},
plugins: [
// Analyze bundle size
new BundleAnalyzerPlugin({
analyzerMode: "static",
openAnalyzer: false
})
]
};Vite Configuration
Vite uses native ES modules during development for instant server start and updates. It's the fastest development experience available and the modern default choice.
// vite.config.js - Vite Configuration
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { resolve } from "path";
export default defineConfig({
// Plugins
plugins: [react()],
// Path aliases
resolve: {
alias: {
"@": resolve(__dirname, "src"),
"@components": resolve(__dirname, "src/components"),
"@utils": resolve(__dirname, "src/utils")
}
},
// Development server
server: {
port: 3000,
open: true,
// Proxy API requests
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true
}
}
},
// Build configuration
build: {
// Target modern browsers
target: "es2018",
// Use esbuild for minification (fastest)
minify: "esbuild",
// Split CSS per chunk
cssCodeSplit: true,
// Disable sourcemaps for production
sourcemap: false,
// Chunk splitting
rollupOptions: {
output: {
manualChunks: {
// Separate vendor chunks
vendor: ["react", "react-dom"],
router: ["react-router-dom"],
query: ["@tanstack/react-query"]
}
}
}
},
// Environment variables prefix
envPrefix: "VITE_"
});✅ Why Vite is Fast: During development, Vite serves raw ES modules to the browser — no bundling required! For production, it uses Rollup with ESBuild for lightning-fast builds.
Writing Vite Plugins
Vite plugins follow Rollup's plugin interface with additional Vite-specific hooks. You can transform code, inject scripts, and customize the build process.
// Writing a Custom Vite Plugin
function myVitePlugin() {
return {
name: "my-vite-plugin",
// Runs during config resolution
config(config, { command }) {
console.log("Building for:", command);
},
// Transform source code
transform(code, id) {
// Convert .txt files to JS modules
if (id.endsWith(".txt")) {
return {
code: `export default ${JSON.stringify(code)}`,
map: null
};
}
},
// Modify the HTML
transformIndexHtml(html) {
return html.replace(
"</head>",
'<script>console.log("Injected!")</script></head>'
);
},
// Build hooks
buildStart() {
console.log("Build started!");
},
buildEnd() {
console.log("Build finished!");
}
};
}
// Use in vite.config.js
export default defineConfig({
plugins: [myVitePlugin()]
});Code Splitting & Lazy Loading
Code splitting creates separate chunks that load on demand, dramatically improving initial load times. Use dynamic imports to load routes, features, and heavy libraries only when needed.
// Code Splitting with Dynamic Imports
// 1. Route-based code splitting
const routes = [
{
path: "/",
// Lazy load the home page
component: () => import("./pages/Home.js")
},
{
path: "/dashboard",
// Lazy load dashboard (admin only)
component: () => import("./pages/Dashboard.js")
},
{
path: "/settings",
component: () => import("./pages/Settings.js")
}
];
// 2. Feature-based code splitting
async function loadChartLibrary() {
// Only load heavy library when needed
const { Chart } = await import("chart.js");
return new Chart(canvas, config);
}
button.addEventListener("click", async () => {
const chart = await loadChartLibrary();
chart.render();
});
// 3. Conditional code splitting
async function loadLocale(lang) {
// Load language pack on demand
const messages = await import(`./locales/${lang}.js`);
return messages.default;
}
// 4. Component-level splitting (React)
const HeavyComponent = React.lazy(() => import("./HeavyComponent"));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}
// 5. Named chunks for better debugging
import(/* webpackChunkName: "admin" */ "./admin/AdminPanel.js");
import(/* webpackChunkName: "charts" */ "./charts/ChartModule.js");Tree Shaking
Tree shaking eliminates unused code from your final bundle. It works best with ES modules and side-effect-free code.
// Tree Shaking - Dead Code Elimination
// ❌ BAD: Imports entire library (no tree shaking)
import _ from "lodash";
const result = _.map([1, 2, 3], x => x * 2);
// ✅ GOOD: Import only what you need (tree shakeable)
import { map } from "lodash-es";
const result = map([1, 2, 3], x => x * 2);
// ❌ BAD: Side effects prevent tree shaking
export function usefulFunction() {
return "I am useful";
}
export function unusedFunction() {
return "I am never used";
}
// Side effect - runs when module is imported!
console.log("Module loaded!");
// ✅ GOOD: Pure modules are fully tree-shakeable
// utils.js - mark as side-effect free in package.json
export function usefulFunction() {
return "I am useful";
}
export function unusedFunction() {
return "I am never used"; // Will be removed!
}
// package.json - Mark package as side-effect free
{
"name": "my-library",
"sideEffects": false
// OR specify files with side effects:
// "sideEffects": ["*.css", "*.scss"]
}
// Tree Shaking Tips:
// 1. Use ES modules (import/export), not CommonJS (require)
// 2. Prefer libraries with ES module builds (*-es packages)
// 3. Avoid side effects in modules
// 4. Use "sideEffects" field in package.json
// 5. Enable minification (removes unused code)Worked Example: A Bundler in Miniature
Tree shaking and code splitting sound like magic until you see the algorithm, which is surprisingly small. A bundler walks your imports from the entry file, keeps everything it reaches, and discards everything it does not. That is it.
The program below does exactly that over a pretend project, and prints the bundle, the dead code and the sizes. Read the comments first, then press Run and check the output against the list at the bottom of the code.
// -- WORKED EXAMPLE - a bundler in miniature --
// Real bundlers do far more than this, but the two jobs at the heart of
// Webpack, Vite and Rollup are exactly the two below: walk the import graph
// from an entry point, then throw away everything you never reached.
// A pretend project. Each "file" lists what it imports, plus a size in KB so
// you can see the effect on the bundle your users have to download.
const project = {
"main.js": { imports: ["cart.js", "format.js"], sizeKb: 2 },
"cart.js": { imports: ["format.js"], sizeKb: 4 },
"format.js": { imports: [], sizeKb: 3 },
"admin.js": { imports: ["charts.js"], sizeKb: 12 },
"charts.js": { imports: [], sizeKb: 40 },
"unused.js": { imports: [], sizeKb: 7 }
};
// JOB 1 - build the module graph. Start at the entry file and follow every
// import it declares, then every import THOSE files declare, and so on.
function collectModules(entry, files) {
const seen = new Set(); // files already pulled in
const queue = [entry]; // files still to inspect
while (queue.length > 0) {
const name = queue.shift(); // take the next file off the front
if (seen.has(name)) continue; // already handled - and this is also
// what stops circular imports looping
seen.add(name);
const file = files[name];
if (!file) continue; // a name with no file = a broken import
file.imports.forEach(dep => queue.push(dep)); // queue what it needs
}
return seen;
}
const included = collectModules("main.js", project);
console.log("in the bundle:", [...included].join(", "));
// JOB 2 - anything the walk never reached is dead code.
// That is tree shaking, and it really is this simple in principle.
const shakenOut = Object.keys(project).filter(name => !included.has(name));
console.log("shaken out: ", shakenOut.join(", "));
// The part your users actually feel: size.
const sumSize = (names) => names.reduce((kb, n) => kb + project[n].sizeKb, 0);
console.log("bundle:", sumSize([...included]) + "kb");
console.log("saved: ", sumSize(shakenOut) + "kb");
// And this is why code splitting exists. Load the admin screen with a dynamic
// import() rather than a normal import, and it becomes its own chunk -
// downloaded only by the people who actually open it.
const adminChunk = collectModules("admin.js", project);
console.log("admin chunk:", [...adminChunk].join(", "));
console.log("admin chunk size:", sumSize([...adminChunk]) + "kb, downloaded on demand");
// Expected output:
// in the bundle: main.js, cart.js, format.js
// shaken out: admin.js, charts.js, unused.js
// bundle: 9kb
// saved: 59kb
// admin chunk: admin.js, charts.js
// admin chunk size: 52kb, downloaded on demandNotice why tree shaking needs ES modules. This walk only works because import and export are static — a bundler can read them without running your code. require(someVariable) can point anywhere at runtime, which is why CommonJS code is so much harder to shake.
🎯 Your Turn — Finish the Tree Shaker
Same idea, different project, and three blanks for you. The numbers in the expected output are the point: an unused legacy.js dragging in a 88kb library is the single most common reason a bundle is bigger than anyone expects.
// 🎯 YOUR TURN - finish the bundler
// Three blanks, all in the two steps that matter: walking the import graph,
// and throwing away whatever the walk never reached.
const project = {
"main.js": { imports: ["cart.js", "format.js"], sizeKb: 2 },
"cart.js": { imports: ["format.js"], sizeKb: 4 },
"format.js": { imports: [], sizeKb: 3 },
"legacy.js": { imports: ["jquery.js"], sizeKb: 9 },
"jquery.js": { imports: [], sizeKb: 88 }
};
function collectModules(entry, files) {
const seen = new Set();
const queue = [entry];
while (queue.length > 0) {
const name = queue.shift();
if (seen.has(name)) continue; // also what stops circular imports looping
seen.add(name);
// 👉 1) Keep the walk going: queue up everything this file imports.
// files[name].imports is an array of file names, and 'dep' is one of them.
files[name].imports.forEach(dep => ___);
}
return seen;
}
const included = collectModules("main.js", project);
console.log("bundled:", [...included].join(", "));
// 👉 2) Dead code is every file in the project that 'included' never reached.
// Hint: a Set has a .has(name) method.
const dead = Object.keys(project).filter(name => ___);
console.log("shaken out:", dead.join(", "));
// 👉 3) Add up the sizeKb of a list of file names.
// 'kb' is the running total so far and 'n' is the current file name.
const sumSize = (names) => names.reduce((kb, n) => ___, 0);
console.log("bundle size:", sumSize([...included]) + "kb");
console.log("removed:", sumSize(dead) + "kb");
// ✅ Expected output:
// bundled: main.js, cart.js, format.js
// shaken out: legacy.js, jquery.js
// bundle size: 9kb
// removed: 97kbBlank 1 is where people get stuck. forEach does not collect anything for you — you have to push each dependency onto queue yourself, or the walk stops after the entry file and your "bundle" is one module long.
Hot Module Replacement (HMR)
HMR updates modules in the browser without a full page refresh, preserving application state. Vite provides instant HMR out of the box.
// Hot Module Replacement (HMR)
// How HMR works internally:
// 1. You edit a file
// 2. Bundler compiles JUST that file
// 3. Sends update via WebSocket
// 4. Browser replaces the module
// 5. State is preserved (no page refresh!)
// Vite - HMR is automatic for most files
// For custom HMR handling:
if (import.meta.hot) {
// Accept updates for this module
import.meta.hot.accept((newModule) => {
console.log("Module updated!", newModule);
});
// Clean up before module is replaced
import.meta.hot.dispose((data) => {
// Save state before update
data.savedState = currentState;
clearInterval(timer);
});
// Access data from previous version
if (import.meta.hot.data.savedState) {
currentState = import.meta.hot.data.savedState;
}
}
// Webpack - Manual HMR API
if (module.hot) {
module.hot.accept("./module.js", () => {
// Handle the updated module
console.log("Module updated!");
});
module.hot.dispose((data) => {
// Cleanup before replacement
});
}
// React Fast Refresh (built into Vite/CRA)
// - Preserves component state during updates
// - Only refreshes changed components
// - Falls back to full reload when needed
// CSS HMR
// Both Vite and Webpack automatically hot-reload CSS
// without losing JavaScript stateEnvironment Variables
Bundlers inject environment-specific variables at build time, allowing different configurations for development, staging, and production.
// Environment Variables in Bundlers
// Vite - Uses import.meta.env
// .env file:
// VITE_API_URL=https://api.example.com
// VITE_DEBUG=true
// Access in code:
console.log(import.meta.env.VITE_API_URL);
console.log(import.meta.env.VITE_DEBUG);
console.log(import.meta.env.MODE); // "development" or "production"
console.log(import.meta.env.DEV); // true in dev
console.log(import.meta.env.PROD); // true in production
// Webpack - Uses DefinePlugin
// webpack.config.js
const webpack = require("webpack");
module.exports = {
plugins: [
new webpack.DefinePlugin({
"process.env.API_URL": JSON.stringify("https://api.example.com"),
"process.env.DEBUG": JSON.stringify(true),
__DEV__: JSON.stringify(process.env.NODE_ENV !== "production")
})
]
};
// Access in code:
console.log(process.env.API_URL);
if (__DEV__) {
console.log("Development mode");
}
// Environment-specific files
// .env - All environments
// .env.local - Local overrides (gitignored)
// .env.development - Development mode
// .env.production - Production mode
// .env.test - Test mode
// Priority (Vite): .env.production.local > .env.production > .env.local > .envModule Federation (Microfrontends)
Webpack Module Federation allows separate applications to share code at runtime — the foundation of microfrontend architecture used by Amazon, Netflix, and Shopify.
// Webpack Module Federation (Microfrontends)
// Host Application - webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "host",
// Import modules from remote apps
remotes: {
// "internal name": "remote_name@URL"
headerApp: "header@http://localhost:3001/remoteEntry.js",
productApp: "products@http://localhost:3002/remoteEntry.js",
cartApp: "cart@http://localhost:3003/remoteEntry.js"
},
// Shared dependencies (avoid duplication)
shared: {
react: { singleton: true, requiredVersion: "^18.0.0" },
"react-dom": { singleton: true, requiredVersion: "^18.0.0" }
}
})
]
};
// Use remote components in host
import React, { Suspense } from "react";
const Header = React.lazy(() => import("headerApp/Header"));
const ProductList = React.lazy(() => import("productApp/ProductList"));
const Cart = React.lazy(() => import("cartApp/Cart"));
function App() {
return (
<div>
<Suspense fallback="Loading header...">
<Header />
</Suspense>
<Suspense fallback="Loading products...">
<ProductList />
</Suspense>
<Suspense fallback="Loading cart...">
<Cart />
</Suspense>
</div>
);
}
// Remote Application - webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "header",
filename: "remoteEntry.js",
// Expose modules to other apps
exposes: {
"./Header": "./src/components/Header.js",
"./Navigation": "./src/components/Navigation.js"
},
shared: {
react: { singleton: true },
"react-dom": { singleton: true }
}
})
]
};Server-Side Rendering (SSR)
SSR requires bundling both client and server code. Vite has built-in SSR support with separate entry points for server rendering and client hydration.
// Server-Side Rendering (SSR) Build Configuration
// Vite SSR - vite.config.js
import { defineConfig } from "vite";
export default defineConfig({
build: {
// SSR entry point
ssr: true,
rollupOptions: {
input: "./src/entry-server.js"
}
},
ssr: {
// Don't bundle these (use Node.js require)
noExternal: ["some-package-to-bundle"]
}
});
// entry-server.js - Server entry point
import { renderToString } from "react-dom/server";
import App from "./App";
export function render(url) {
const html = renderToString(<App url={url} />);
return html;
}
// entry-client.js - Client entry point (hydration)
import { hydrateRoot } from "react-dom/client";
import App from "./App";
hydrateRoot(document.getElementById("root"), <App />);
// Express server example
import express from "express";
import { render } from "./dist/server/entry-server.js";
const app = express();
app.use("*", async (req, res) => {
const appHtml = render(req.originalUrl);
const html = `
<!DOCTYPE html>
<html>
<head><title>SSR App</title></head>
<body>
<div id="root">${appHtml}</div>
<script type="module" src="/entry-client.js"></script>
</body>
</html>
`;
res.status(200).set({ "Content-Type": "text/html" }).end(html);
});
app.listen(3000);Parcel: Zero Configuration
Parcel automatically detects and handles all file types without configuration. Perfect for prototypes, learning, and simple projects.
// Parcel - Zero Configuration Bundler
// No config file needed! Just run:
// npx parcel index.html
// Parcel automatically detects and handles:
// - JavaScript/TypeScript
// - JSX/TSX
// - CSS/SCSS/LESS
// - Images and fonts
// - HTML
// Optional .parcelrc for customization
{
"extends": "@parcel/config-default",
"transformers": {
"*.svg": ["@parcel/transformer-svg-react"]
},
"optimizers": {
"*.js": ["@parcel/optimizer-terser"]
}
}
// package.json scripts
{
"scripts": {
"dev": "parcel src/index.html",
"build": "parcel build src/index.html",
"clean": "rm -rf dist .parcel-cache"
},
// Configure build targets
"targets": {
"default": {
"distDir": "./dist",
"publicUrl": "./"
}
},
// Browser targets
"browserslist": "> 0.5%, last 2 versions, not dead"
}
// Multi-entry builds
// npx parcel src/index.html src/admin.html
// Library mode
{
"main": "dist/main.js",
"module": "dist/module.js",
"source": "src/index.js",
"targets": {
"main": { "includeNodeModules": false },
"module": { "includeNodeModules": false }
}
}Choosing the Right Bundler
Each bundler excels in different scenarios. Choose based on your project's needs, team experience, and long-term requirements.
🏆 Mini-Challenge: Enforce a Performance Budget
No blanks now — a brief, an outline and a test harness. A performance budget is the cheapest bundle-size protection a team can have: agree a limit per chunk, then fail the build when someone crosses it. Every bundler can do this; here you write the check yourself.
The vendor line is the realistic one. Nobody writes 137kb of vendor code on purpose; it accumulates one npm install at a time, which is exactly why the check has to be automatic rather than something a person remembers to do.
Key Takeaways
Core Concepts
- ✓ Entry points, loaders, and plugins
- ✓ Code splitting with dynamic imports
- ✓ Tree shaking for dead code elimination
- ✓ Hot Module Replacement (HMR)
- ✓ Environment-specific builds
When to Use Each
- ✓ Vite — New projects, fastest dev
- ✓ Webpack — Enterprise, microfrontends
- ✓ Parcel — Prototypes, learning
- ✓ Production: minify, split, cache, CDN
Practice quiz
Why do modern apps need module bundlers, according to the lesson?
- Browsers can't natively handle ES modules, TypeScript, JSX, SCSS, and hundreds of dependencies
- Bundlers make code run on the server
- Browsers refuse to load JavaScript without them
- They are only needed for CSS
Answer: Browsers can't natively handle ES modules, TypeScript, JSX, SCSS, and hundreds of dependencies. Bundlers transform development code (ES modules, TS, JSX, SCSS, many deps) into optimized, browser-ready production builds.
Which bundler does the lesson call the fastest dev experience and the modern default?
- Webpack
- Parcel
- Vite
- Rollup
Answer: Vite. Vite serves native ES modules in development for instant startup, making it the fastest dev experience and the modern default choice.
What does tree shaking do?
- Combines all CSS into one file
- Eliminates unused (dead) code from the final bundle
- Splits code into lazy-loaded chunks
- Adds source maps for debugging
Answer: Eliminates unused (dead) code from the final bundle. Tree shaking eliminates unused code from the final bundle and works best with ES modules and side-effect-free code.
Which bundler is described as requiring zero configuration?
- Webpack
- Vite
- Rollup
- Parcel
Answer: Parcel. Parcel is the zero-configuration bundler; you just run it and it auto-detects and handles all file types.
What enables code splitting so chunks load on demand?
- The require() function
- Dynamic imports (import())
- The eval() function
- CSS @import rules
Answer: Dynamic imports (import()). Code splitting uses dynamic imports like import('./pages/Home.js') to load routes, features, and heavy libraries only when needed.
What does Hot Module Replacement (HMR) do?
- Minifies code on each save
- Updates modules in the browser without a full page refresh, preserving state
- Compresses bundles with Brotli
- Removes unused code at build time
Answer: Updates modules in the browser without a full page refresh, preserving state. HMR updates modules in the browser without a full refresh, preserving application state. Vite provides instant HMR out of the box.
In Vite, how do you access environment variables in code?
- process.env.X
- import.meta.env.X
- window.env.X
- global.env.X
Answer: import.meta.env.X. Vite exposes env variables via import.meta.env (e.g. import.meta.env.VITE_API_URL), while Webpack uses DefinePlugin with process.env.
Which Webpack feature is the foundation of microfrontend architecture?
- Module Federation
- Tree shaking
- Source maps
- DevServer proxy
Answer: Module Federation. Webpack Module Federation lets separate applications share code at runtime, which is the basis of microfrontends used by Amazon, Netflix, and Shopify.
For tree shaking, which module system should you use?
- CommonJS (require)
- ES modules (import/export)
- AMD modules
- UMD modules
Answer: ES modules (import/export). The lesson's tree-shaking tips say to use ES modules (import/export) rather than CommonJS (require) so unused code can be removed.
What three things do a Webpack config's entry, loaders, and plugins control?
- Where bundling starts, how files are transformed, and extra functionality
- Server port, database, and routing
- Only CSS, fonts, and images
- Testing, linting, and formatting
Answer: Where bundling starts, how files are transformed, and extra functionality. Entry points define where bundling starts, loaders transform files (babel-loader, css-loader), and plugins extend functionality (HtmlWebpackPlugin, etc.).
Continue this course
- Previous: Building Reusable UI Components with Pure JS
- Next: Intro to TypeScript & Strong Typing Concepts — Add static types to JavaScript with TypeScript and avoid runtime bugs
- Quick reference: JavaScript cheat sheet