LearnThatStack Ace your next interview

Vite & Webpack

Vite 8 with Rolldown, and webpack 5 Checked Oct 2026 104 code snippets

The model

  • A bundler starts at an entry file and follows every import to build a dependency graph. It writes that graph out as bundles, the files the browser loads.
  • Webpack bundles the whole app before it serves a page, in dev and in production. Loaders transform one file at a time, and plugins hook into the whole build.
  • Vite skips the bundle in dev. It serves your code as native ES modules, compiles each file when the browser asks for it, and pre-bundles node_modules once.
  • For production, Vite bundles anyway, because each unbundled import costs a network round trip. Vite 8 uses Rolldown, a Rust bundler. Earlier versions built with Rollup.
  • Hot Module Replacement (HMR) swaps an edited module into the running page, so its state survives. If no module accepts the update, the page reloads.
Browsernative ES modulesVite dev servercompiles on requestnode_modulespre-bundled onceYour sourcesrc/, index.htmlSTARTpre-bundle at startupREQUESTimport App.jsxtransform one fileES moduleEDITfile savedHMR update WebSocket
Vite pre-bundles dependencies once, then compiles a source file only when the browser imports it. Webpack bundles the whole app before it serves anything.

How bundlers work

the ideas behind every Vite and webpack question

Why Vite's dev server starts fast

webpackbundle the appreadyVitedepsfilefilefilereadytime

Webpack is ready only after it bundles the app. Vite is ready once dependencies are pre-bundled, then compiles each file when the browser asks.

Explain Vite's fast startNo bundle. It compiles each file when the browser imports it.
Name the cost of skipping the bundleOne request per module, so a huge app loads slowly in dev.

Pre-bundling and production bundles

Say why Vite pre-bundles node_modulesIt turns CommonJS into ES modules, and many files into one request.
Always pre-bundle a packageoptimizeDeps.include
Rebuild node_modules/.vitevite --force
Say why production needs a bundleEach import is a network round trip, and nested ones wait in a chain.
Say what Vite 8 changedRolldown now pre-bundles and builds. Before, esbuild and Rollup did.

Start a Vite project

create, run and build an app, and know where files go

Where files go

index.htmlthe entry, in the rootpublic/copied as issrc/main.jswhere your code startssrc/assets/imported, then hashedvite.config.jsthe config
Load your code from index.html<script type="module" src="/src/main.js">
Serve a file untouchedpublic/robots.txtLink to it as /robots.txt.

Trapindex.html goes in the root, not in public/. Vite treats it as source, part of the module graph.

Configure Vite

the config file, modes, env variables, TypeScript and tests

Env variables in Vite

Expose a variable to the browserVITE_API_URL=/api
Read it in code, always as a stringimport.meta.env.VITE_API_URL
Know which env file wins.env.[mode] beats .env. Shell variables beat both.

TrapOnly VITE_ names reach the browser, and anyone can read them in the bundle. Never put a secret in one.

TypeScript and path aliases

Type-check, since Vite only strips typestsc --noEmit && vite build
Set the tsconfig flag Vite needs"isolatedModules": true
Import from @/ instead of ../../resolve.alias
Reuse tsconfig.json aliases in Vite 8resolve.tsconfigPaths: true

TrapVite's alias doesn't reach TypeScript. Add matching paths to tsconfig.json, or tsc reports missing imports.

Configure webpack

dev and production configs, env values and transpiling

Dev, production and env values

Share a base config for dev and prodmerge(common, prod)From webpack-merge.
Put a value into the bundle at build timenew webpack.DefinePlugin({})
Fail the build if a variable is missingnew webpack.EnvironmentPlugin(['API_URL'])

TrapDefinePlugin pastes each value in as code. Wrap a string in JSON.stringify, or it arrives without its quotes.

Externals, workers and top-level await

Load React from a CDN insteadexternals: { react: 'React' }
Bundle a Web Worker without worker-loadernew Worker(new URL('./w.js', import.meta.url))
Use top-level awaitNo config needed since webpack 5.83.

TrapKeep the worker path a literal string in new URL. Webpack can't follow a variable.

CSS, images & fonts

load styles and files in each tool

CSS in Vite

Use Sass, Less or Stylusnpm install -D sassNo plugin needed.
Inject shared Sass variablescss.preprocessorOptions
Scope classes to one componentButton.module.css

TrapPut @use, not @import, in scss.additionalData. Sass deprecated @import.

Dev server & HMR

swap modules live, proxy API calls, keep the server safe

Dev servers, proxies and safety

Proxy /api to a backend in Viteserver.proxyThe browser sees one origin, so no CORS check runs.
Start webpack-dev-servernpx webpack serve
Proxy /api in webpack-dev-server 5Give proxy an array of objects, each with a context list.
Serve index.html for client-side routeshistoryApiFallback: true
Open Vite's dev server to your phoneserver.host: true
Limit what Vite's dev server can serveserver.fs.allow

TrapA dev proxy does nothing in production. Route /api on your web server, or send CORS headers.

Split & lazy-load code

send less JavaScript on the first load

Dynamic import()

Load a module in its own chunkawait import('./chart.js')
Lazy-load a whole folder in Viteimport.meta.glob('./pages/*.js')
Name or prefetch a webpack chunk/* webpackChunkName: "chart" *//* webpackPrefetch: true */

TrapIn webpack, a fully dynamic import(foo) bundles nothing and fails at runtime. Keep part of the path static.

Code splitting in Vite

Say where Vite splitsAt each entry and import(). Shared code gets its own chunk.
Group libraries into a chunk in Vite 8codeSplitting.groups
Group modules with manualChunksIts object form is gone in Vite 8. Its function form is deprecated.
Ship one CSS file, not one per chunkbuild.cssCodeSplit: false

TipVite loads a lazy chunk's CSS before it runs the chunk. That prevents a flash of unstyled content.

Tree shaking

Keep code tree-shakableUse ES modules. Babel must not turn import into require.
Turn tree shaking onmode: 'production'It marks unused exports. The minifier deletes them.
Let webpack drop unused files whole"sideEffects": false

TrapList polyfills and CSS in sideEffects. Otherwise webpack may drop them.

Caching, speed & debugging

browser caching, faster builds and build errors

Content hashes and cache headers

Cache files until they change[name].[contenthash].jsProduction mode hashes each file's final bytes.
Set cache headers for a deployHashed files get max-age=31536000, immutable. index.html gets no-cache.

Speed up a Vite build

Minify with the defaultsVite 8 uses Oxc for JS and Lightning CSS for CSS.
Switch to Terser to drop console callsminify: 'terser'drop_console: true
Skip gzip size reportsreportCompressedSize: false
Load fewer files per pageImport from the file itself, not a barrel index.js.

TrapoptimizeDeps can't speed up vite build. Pre-bundling only runs in dev.

Analyze and debug builds

See what's in each webpack chunkwebpack --profile --json > stats.jsonwebpack-bundle-analyzer stats.json
Get the full error and import chainstats: { errorDetails: true }
Fix the error Can't resolve 'crypto'resolve.fallbackWebpack 5 stopped adding Node.js polyfills.
Profile a slow Vite buildvite build --profile

Source maps

Pick a dev source mapdevtool: 'eval-source-map'
Give source maps to error tools onlydevtool: 'hidden-source-map'
Read the output of a failing Vite buildbuild: { minify: false, sourcemap: true }

TrapPublic .map files expose your source. Keep them from normal users.

Plugins & loaders

use them, order them and write your own

Vite plugins

Add a pluginplugins: [react()]Most Rollup plugins work too.
Run a plugin only in builds, or firstapply: 'build'enforce: 'pre'
Serve a virtual module, with no file on diskresolveIdload
Compile a custom file typetransform(code, id)
Add a route to the dev serverconfigureServer(server)
Handle HMR for a custom file typehandleHotUpdate

Webpack loaders and plugins

Tell a loader from a pluginA loader changes one file. A plugin hooks into the whole build.
Generate the HTML that loads your bundlesnew HtmlWebpackPlugin()
Empty dist/ before each buildoutput.clean: true
Write a loaderthis.getOptions()Export a function that takes the source and returns new source.
Write a pluginapply(compiler)Tap the hooks you need.
Add a file to the outputcompilation.hooks.processAssets

TrapWebpack caches loader results, so a loader must give the same output for the same input.

Deploy, scale & migrate

deploys, libraries, SSR, micro-frontends and upgrades

Module Federation

Load another app's modules at runtimeModuleFederationPluginA remote exposes modules. A host loads them.
Run one copy of React across appssingleton: true
Fix "not available for eager consumption"Start the entry with import('./bootstrap').
Use Module Federation with Vite@module-federation/vite

TrapA remote that is down or changes its API breaks the host at runtime. Plan a fallback.

Migrate and upgrade

Port webpack-only code to Viterequire.context becomes import.meta.glob. process.env.X becomes import.meta.env.VITE_X.
Upgrade webpack 4 to 5Clear every deprecation warning on 4 first. Webpack 5 removed everything 4 deprecated.
Close the compiler after a Node.js API buildcompiler.close(callback)
Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 89 questions it links, not just the 15 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.