
Here is a bug that explains why frontend build tools interview questions exist at all. A component throws Element type is invalid because a CommonJS dependency's default import resolves to the module.exports object when the application expected module.exports.default. Vite's troubleshooting guide (https://vite.dev/guide/troubleshooting) documents this CJS interoperability trap directly. Historically, Vite could handle this differently between development and production; Vite 8 made that behavior consistent across both modes.
The broader subject is still the same: development and production do different work. Vite's default dev server serves source files on demand over native ES modules, while the production build bundles and optimizes the complete module graph. Understanding where those modes differ is more useful than memorizing configuration.
A word on how to read this guide. Build tooling is a genuine interview topic but a peripheral one. Where it does surface, it reads more like a conversational probe ("what does a bundler actually do", "how would you structure this for micro-frontends") than a config-recall drill. Published prep material also covers build tooling far more heavily than first-hand write-ups of real interview loops do, which is worth knowing before you spend a week on it. And there is a fair argument against the topic itself: asking someone to recall webpack flags from memory measures recall, not engineering judgment. This guide is written for the version of the question that is worth asking: can you reason about the pipeline you ship through.
Two numbers set the context, and both need their caveats stated.
In the State of JS 2025 survey (https://2025.stateofjs.com/en-US/libraries/build-tools/), which ran from 24 September to 11 November 2025 and collected 13,002 responses, 87.18% of respondents said they had used webpack and 84.61% said they had used Vite. Rolldown sat at 10.67% and Rspack at 7.47%. The important caveat: that survey closed roughly four months before Vite 8 and Rolldown reached stable, so the Rolldown and Rspack figures are pre-stable snapshots and understate where those tools are now.
The npm registry gives a second, more current angle. In the week ending 21 September 2026, webpack was downloaded about 43.4 million times and Vite about 131.2 million. Download counts include continuous integration runs and transitive installs, so they measure presence in dependency trees rather than deliberate choice. Read together, they support one modest claim: both tools are widely present in real codebases, and neither one is a safe thing to be blank about.
That is also why this is worth an afternoon rather than a week. Writing a webpack.config.js from scratch under interview conditions is an unusual ask. Explaining why a build behaves the way it does is the more portable skill, and it is the one this guide drills.
If you want structured practice across all the formats a frontend loop uses, GreatFrontEnd's Front End Interview Playbook (https://www.greatfrontend.com/front-end-interview-playbook) covers coding, system design and quiz-style rounds, which is the round format this kind of question fits.
Start with the module graph. A bundler takes one or more entry points, follows every import and require statement to build a graph of modules, applies transformations to each one, then emits a smaller number of output files.
The part worth naming is the two-stage pipeline inside that process, because webpack's documentation draws the line clearly. Loaders (https://webpack.js.org/concepts/loaders/) are per-file transformations: "Loaders are transformations that are applied to the source code of a module. They allow you to pre-process files as you import or 'load' them." Plugins (https://webpack.js.org/concepts/plugins/) operate on everything else. A plugin is "a JavaScript object that has an apply method", and that method "is called by the webpack compiler, giving access to the entire compilation lifecycle."
A detail that separates a careful answer from a vague one is loader ordering. The docs state that "Loaders are evaluated/executed from right to left (or from bottom to top)", so in this config sass-loader runs first and hands its output to postcss-loader:
export default {module: {rules: [{test: /\.scss$/,use: [{ loader: "postcss-loader" },{ loader: "sass-loader", options: { sourceMap: true } },],type: "css/auto",},],},};
This is the highest-signal question in the whole topic, because the answer explains a class of bugs rather than a single feature.
In development, Vite serves source files over native ES modules and lets the browser do the graph traversal. That is what makes cold start feel instant: nothing has to be bundled up front. For production it runs a real bundler over the module graph, because shipping hundreds of separate module requests to a real user on a real network is a different problem from serving them to a browser on localhost.
Dependencies are the exception, and dependency pre-bundling (https://vite.dev/guide/dep-pre-bundling) is where to show depth. Vite pre-bundles node_modules for two reasons its docs spell out. First, CommonJS and UMD packages have to be converted to ESM before the browser can load them. Second, performance: "Some packages ship their ES modules builds as many separate files importing one another. For example, lodash-es has over 600 internal modules!" Without pre-bundling, one import of lodash-es becomes 600-plus requests. The optimizeDeps option is how you correct the automatic discovery when an import is not statically visible in your source.
Historically, that dev/build boundary caused CommonJS interoperability differences, but Vite 8 specifically made default-import handling consistent (https://vite.dev/guide/migration) across the two modes. A current answer should still understand that development serves source on demand while production bundles the graph, without presenting CJS default-import semantics as an expected difference between them.
"It removes unused code" is the floor. The useful answer starts one level down, with why it is possible at all. In webpack's words (https://webpack.js.org/guides/tree-shaking/), tree shaking "relies on the static structure of ES2015 module syntax, i.e. import and export." Static means the bundler can determine the import and export bindings without executing the module. require() calls can be computed at runtime, so the same analysis is not available.
The second half of the answer is that webpack has two separate optimizations, not one. usedExports marks individual exports as unused and leaves the deletion to the minifier. sideEffects is a declaration from a package that its modules can be dropped whole. The docs are explicit about the gap: "sideEffects is much more effective since it allows to skip whole modules/files and the complete subtree", whereas "usedExports relies on terser to detect side effects in statements", which "is a difficult task in JavaScript."
The senior tell is naming the failure cases, and these are documented production mistakes rather than anything observed about candidates:
sideEffects: false that is not true. Any file imported purely for its effect has to be declared, or it can be dropped. The webpack docs give the exact shape, with CSS called out by name:{"name": "your-project","sideEffects": ["./src/some-side-effectful-file.js", "*.css"]}
usedExports analysis and minimization by default, while development mode does not, so a normal development build is not a reliable way to judge the final tree-shaken output. Those optimizations can still be enabled explicitly outside production mode.Tree shaking is one piece of a larger payload story. GreatFrontEnd's guide to front-end performance techniques (https://www.greatfrontend.com/blog/front-end-performance-techniques) covers the shipping side, including compression and resource hints, that sits downstream of everything here.
This is the cleanest discriminator in the topic, because the naive answer is a real answer that is simply incomplete.
Content hashing works as advertised: "The [contenthash] substitution will add a unique hash based on the content of an asset. When the asset's content changes, [contenthash] will change as well." The problem is that two other things also change the content, and neither is your source code.
The first is the runtime. Webpack's runtime and manifest contain information about the module and chunk graph. When that graph changes, the runtime can change even if the application code in a particular entry did not. If that runtime lives inside the entry chunk, its changed content also changes the entry chunk's [contenthash]. Setting optimization.runtimeChunk to 'single' moves the runtime into its own chunk, preventing that metadata churn from unnecessarily invalidating the entry chunk.
The second is module identifiers. If module IDs are assigned by resolution order, adding one import anywhere can renumber modules inside your vendor chunk and invalidate a file whose actual dependencies did not change. optimization.moduleIds: 'deterministic' pins them. Production mode already defaults to 'deterministic', so setting it explicitly is about keeping the ids stable regardless of mode.
The worked configuration, straight from webpack's caching guide (https://webpack.js.org/guides/caching/):
module.exports = {output: {filename: '[name].[contenthash].js',path: path.resolve(__dirname, 'dist'),clean: true,},optimization: {moduleIds: 'deterministic',runtimeChunk: 'single',splitChunks: {cacheGroups: {vendor: {test: /[\\/]node_modules[\\/]/,name: 'vendors',chunks: 'all',},},},},};
The trap here is reasoning from HTTP/1.1. Under HTTP/1.1, browsers held roughly six connections per origin, so every extra file was a real queuing cost and "bundle everything into one file" was sound advice. HTTP/2 multiplexes requests over a single connection, which removed that ceiling. The constraint moved to compression ratio, which favours larger files, and cache granularity, which favours smaller ones.
There is measured evidence, and it needs its date attached. Google's work on granular chunking in Next.js (https://web.dev/articles/granular-chunking-nextjs) found that varying the maximum initial request count from 5 to 15 left load, start render and First Contentful Paint "about the same", with a slight overhead appearing "only after splitting aggressively to hundreds of requests". They settled on 25 as the maxInitialRequests value. That article was last updated on 29 April 2020. The mechanism still holds; treat the specific numbers as dated rather than current guidance.
For contrast, webpack's own splitChunks defaults (https://webpack.js.org/plugins/split-chunks-plugin/) today are chunks: "async" with maxInitialRequests: 30 and maxAsyncRequests: 30 in production. Note the first one: by default, only asynchronously loaded chunks are split at all.
This is also where the vendor-chunk pattern deserves a second look. One big vendors chunk containing everything from node_modules is easy to configure and is the version most starter configs ship with. It also means a single dependency bump invalidates the whole thing for every returning user. Splitting by package or by change frequency costs more configuration and buys better cache retention. Which trade is right depends on how often your dependencies actually move, and saying so is a better answer than picking a side.
Chunk boundaries are decided as much in application code as in config. GreatFrontEnd's guide to code splitting and lazy loading in React (https://www.greatfrontend.com/blog/code-splitting-and-lazy-loading-in-react) covers the route and component level decisions that produce the split points a bundler then acts on.
This one rewards range, because there are two separate axes and a complete answer covers both. Most discussion of source maps stops at the first.
The first is the cost and fidelity trade. Webpack's devtool table (https://webpack.js.org/configuration/devtool/) is the reference: source-map is the "Recommended choice for production builds with high quality SourceMaps", while eval-cheap-module-source-map is described as the "Tradeoff choice for development builds" because it rebuilds fast at the cost of column-level accuracy.
The second axis is security, and it is the one that separates a senior answer. Shipping a full source map publishes your original source to anyone who opens DevTools. Two production variants exist precisely for this:
hidden-source-map generates the map but emits no reference comment in the bundle. The docs describe it as a "Possible choice when using SourceMap only for error reporting purposes", with the explicit warning: "You should not deploy the Source Map file to the webserver. Instead only use it for error report tooling." You upload it to Sentry or similar and keep it off the CDN.nosources-source-map ships the map with the source content stripped, so stack traces resolve to real file names and line numbers without exposing the code. The docs note it "still exposes filenames and structure for decompiling, but it doesn't expose the original code."One more detail that reads as genuinely current. The source map format is a published standard: ECMA-426 (https://ecma-international.org/publications-and-standards/standards/ecma-426/), 1st edition, December 2024. Work continues in the TC39 source map group (https://github.com/tc39/source-map), where the Scopes proposal is at Stage 3 and Debug ID is at Stage 2. Knowing that the format stopped being a de facto convention is a small thing that signals you follow the platform.
Three stages that run at different times and answer different questions. Collapsing them into one is where the concept commonly gets muddled, and a single tool doing two of them does not make them one step.
Transpilation rewrites syntax to match a target environment. It is per-file work, which is why it can be fast and why it has a specific limitation worth naming. Vite's docs (https://vite.dev/guide/features) are blunt: "Vite only performs transpilation on .ts files and does NOT perform type checking", because "Transpilation can work on a per-file basis and aligns perfectly with Vite's on-demand compile model. In comparison, type checking requires knowledge of the entire module graph." That is also why you must set "isolatedModules": true in tsconfig.json, and why tsc --noEmit remains a separate step in the pipeline. Assuming your bundler type-checks your code is a common production mistake, which makes it a good interview probe.
Minification rewrites your JavaScript into smaller equivalent JavaScript: shorter identifiers, dropped whitespace, collapsed expressions. It runs at build time and its output is what gets deployed. In Vite 8, build.minify (https://vite.dev/config/build-options) defaults to 'oxc' for the client build and false for SSR, and it also accepts 'terser' and 'esbuild'.
Compression is transport-level. Gzip or Brotli encode the already-minified bytes for transfer, and the browser decodes them back to byte-identical text. It is usually handled by your server or CDN, though some builds also emit pre-compressed .br or .gz files alongside the originals. Either way, it does not change what the browser eventually parses.
Targets are the thread connecting transpilation to everything downstream. Vite 8's build.target defaults to 'baseline-widely-available', which ties output syntax to what is broadly supported across browsers instead of a hand-maintained version list. A tighter target means less transpiled output and a smaller bundle; a looser one means wider reach. That is a product decision wearing a config flag.
Resist naming a tool. Start by separating the three things "slow" can mean: cold start, incremental rebuild on save, and full production build in continuous integration. They have different causes and different fixes.
For each, here is a concrete, checkable thing to look at.
Caching that is not on. Webpack's filesystem cache (https://webpack.js.org/configuration/cache/) is not enabled by default in production: cache defaults to false in production mode and { type: 'memory' } in development. If nobody turned it on, there is nothing to warm. And when you do turn it on, cache.buildDependencies matters, because the docs recommend setting cache.buildDependencies.config: [__filename] so a config change actually invalidates the cache. Without it, a stale cache can survive a config edit quietly.
Caching that is on but thrown away. Next.js 16.3 (https://nextjs.org/blog/next-16-3-turbopack), released on 29 June 2026, enabled Turbopack's persistent filesystem cache for next build by default. The senior detail is one sentence from that release post: "The cache lives in .next/cache, so CI builds only get faster if that directory is restored between runs." A build cache that your CI wipes on every run is a cache in name only.
Barrel files. Next.js documents the shape of the problem (https://nextjs.org/docs/app/api-reference/config/next-config-js/optimizePackageImports): "Some packages can export hundreds or thousands of modules, which can cause performance issues in development and production." Its experimental.optimizePackageImports option ships a hardcoded list of pre-optimized packages including lucide-react, @mui/icons-material, lodash-es and react-icons/*. That option is still marked experimental, and the docs say so directly. The general point stands beyond Next.js: a single import { X } from 'some-icons' against a barrel file can pull an enormous module graph into your dev server.
The reasoning pattern here, measure before you change anything, is the same one senior performance rounds test. GreatFrontEnd's senior web performance developer interview questions (https://www.greatfrontend.com/blog/senior-web-performance-developer-interview-questions-advanced-topics-and-answers) covers that diagnostic discipline in the runtime context, and web performance interview questions (https://www.greatfrontend.com/blog/web-performance-interview-questions) covers the metric fundamentals underneath it.
The architecture conversation and the build conversation are different, and this phrasing points at the second one.
Webpack's ModuleFederationPlugin (https://webpack.js.org/concepts/module-federation/) is the concrete mechanism. It combines ContainerPlugin and ContainerReferencePlugin, and the options map onto the roles directly: name is "the name of the container, which is also the global a consumer reads it from", filename is "the filename of the container entry", exposes lists the modules this build provides, and remotes lists containers to consume, written as name: "container@url".
The option that actually decides whether the system works is shared. Webpack defines shared modules as modules a build "both provides into a share scope and consumes from it, instead of bundling its own private copy." Two per-module settings are worth naming: singleton, which forces a single instance across the whole page, and requiredVersion, which constrains what is acceptable to share.
Why that matters in practice: React's own docs state the condition directly. For hooks to work, "the react import from your application code needs to resolve to the same module as the react import from inside the react-dom package", and if those resolve to two different exports objects you get the invalid hook call warning (https://react.dev/warnings/invalid-hook-call-warning). Two federated remotes each bundling their own react is exactly that situation, which is why React is typically marked as a singleton. Mark everything as a singleton, though, and you have coupled every team's upgrade schedule together, which is the thing micro-frontends were meant to avoid. Naming that tension is a better answer than reciting the config.
For the architectural layer above this, GreatFrontEnd's micro-frontends interview questions (https://www.greatfrontend.com/blog/micro-frontends-interview-questions) covers the ownership and integration decisions that determine whether Module Federation is the right tool in the first place.
In application code, the standard way to create an asynchronous code-split point is a dynamic import(). A bundler can also create additional chunks through multiple entry points, shared-chunk extraction such as SplitChunksPlugin, and framework or tool-specific splitting rules.
webpack's code splitting guide (https://webpack.js.org/guides/code-splitting/) names it directly: of the two supported techniques for dynamic splitting, "The first and recommended approach is to use the import() syntax that conforms to the ECMAScript proposal for dynamic imports." A static import at the top of a module says "I need this before I run." A dynamic import() returns a promise, which is what gives the bundler a boundary it can emit as a separate chunk.
The mid-level answer stops there. Two things separate a senior one.
First, splitting by entry point has a pitfall the same guide warns about: "If there are any duplicated modules between entry chunks they will be included in both bundles." Two entries that both import the same date library ship it twice. That is the problem SplitChunksPlugin exists to solve, and the docs describe the fix in those terms, that the plugin "should notice that we've separated lodash out to a separate chunk and remove the dead weight from our main bundle."
Second, a split point has to be statically analyzable enough for the bundler to know what to emit at build time. Vite documents the limit on dynamic imports built from a variable: "variables only represent file names one level deep." A fully computed specifier gives the bundler nothing to pre-plan.
Worth naming because it is easy to miss: splitting adds a network round trip, and good bundlers try to pay it back. Vite "automatically rewrites code-split dynamic import calls with a preload step" so a chunk's own dependencies are fetched in parallel rather than discovered one at a time, and it "automatically generates <link rel="modulepreload"> directives for entry chunks." A senior answer knows the waterfall exists and that the tool is already fighting it.
exports field change what consumers of a package can import?This is a question about encapsulation, and the reason it belongs in a build-tooling round is that adding the field to an existing package is a breaking change most people do not expect.
Node's packages documentation (https://nodejs.org/api/packages.html) describes exports as a modern alternative to main that allows multiple entry points and conditional resolution, while "preventing any other entry points besides those defined in exports." The consequence is stated plainly: "When the exports field is defined, all subpaths of the package are encapsulated and no longer available to importers", and reaching for one throws ERR_PACKAGE_PATH_NOT_EXPORTED.
The specific trap worth naming is how far that reaches. The docs warn that adding the field "will prevent consumers of the package from using any entry points that are not defined, including the package.json", so a consumer reading a version number out of your-package/package.json breaks. The docs call that out as likely to be a breaking change.
Conditional exports are the second half, and the senior detail is that order is load-bearing rather than cosmetic: "Within the exports object, key order is significant. During condition matching, earlier entries have higher priority and take precedence over later entries", with the general rule that conditions run most specific to least specific. "default" "should always come last", because it always matches, so anything after it is unreachable.
"import" and "require" are documented as always mutually exclusive, which is what lets one package serve both module systems. Node's own docs note that using the two conditions together "can lead to some hazards" and point at their dual CommonJS/ES module packages section rather than treating it as free. The practical shape of that hazard is shipping two copies of the same module with separate internal state, which is the same failure as the singleton problem in Question 9.
Hot Module Replacement gets named constantly and explained rarely, which makes it a fair probe.
webpack's HMR concepts page (https://webpack.js.org/concepts/hot-module-replacement/) splits it into what the application does and what the compiler does. In the application: the code asks the HMR runtime to check for updates, the runtime downloads them asynchronously and notifies the application, the application asks the runtime to apply them, and the runtime applies them synchronously.
On the compiler side an update is two things, not one: an updated manifest in JSON, and one or more updated chunks of JavaScript. The manifest lists the ids of the updated chunks, the removed chunks and the removed modules, and the update for the runtime chunk carries the new compilation hash. That hash is how the next check knows where it is starting from.
The part that explains the behaviour developers actually notice is that HMR is opt-in per module. It only affects modules containing HMR code. If the module you changed has no handler, the update does not stop there: webpack walks from the changed module up through the modules that imported it until it finds one that accepts the change.
That single mechanism explains the day-to-day experience. Editing a component keeps your form state because the framework's HMR integration accepts updates at the component boundary. Editing a file that everything imports, or the application entry, bubbles the update all the way up, and when nothing accepts it you get a full reload and lose that state. Being able to say "nothing in that chain accepted the update, so it bubbled to the root" is the answer that shows the mechanism rather than the feature name.
The instinct to resist is guessing. The bundle is a build artifact you can inspect directly, and an answer that names a library to remove before looking at anything is the same jump-to-a-fix pattern Question 8 penalises.
webpack's code splitting guide (https://webpack.js.org/guides/code-splitting/) frames the purpose precisely: "Once you start splitting your code, it can be useful to analyze the output to check where modules have ended up." It points first at its official analyze tool, then lists community options. The one most often reached for, webpack-bundle-analyzer, is described there as "a plugin and CLI utility that represents bundle content as a convenient interactive zoomable treemap." The same list names webpack-visualizer for seeing "which modules are taking up space and which might be duplicates", and bundle-stats for comparing "the results between different builds".
That last one is the senior move. A single treemap tells you what is big. A diff between two builds tells you what changed, which is the question you actually have when a bundle grew and nobody knows why.
Three things worth looking for, rather than just naming a tool:
The answer that matters is that they are not read at runtime at all. There is no environment in a browser, so the build substitutes text.
Vite's env and mode documentation (https://vite.dev/guide/env-and-mode) is explicit about the mechanism: the built-in constants "are defined as global variables during dev and statically replaced at build time to make tree-shaking effective." Static replacement is what lets a check like import.meta.env.PROD collapse to a constant, so a minifier can delete the dead branch entirely. The built-ins are MODE, BASE_URL, PROD, DEV and SSR.
The exposure rule is a prefix, and the docs put the reasoning the right way round: "Variables prefixed with VITE_ will be exposed in client-side source code after Vite bundling. To prevent accidentally leaking env variables to the client, avoid using this prefix." The default is private. The prefix is how a value opts in.
The security consequence is stated rather than implied: "VITE_* variables should not contain sensitive information such as API keys. The values of these variables are bundled into your source code at build time." A key in a VITE_ variable is not obscured by the build, it is written into a file anyone can open. If one does end up there, rotating it means rebuilding and redeploying, because the old value is still sitting in whatever bundle is cached at the edge.
Two adjacent things a senior answer gets right. process.env habits from webpack's DefinePlugin do not transfer, since Vite exposes import.meta.env instead. And a value that genuinely has to stay secret cannot be a client build-time variable at all, whatever it is named. It belongs behind a server route, which turns a config question into an architecture one.
import './styles.css' in a JavaScript file?Nothing in the language says a JavaScript module can import CSS. That is entirely a build-tool convention, and knowing that development and production answer it differently is the point of the question.
In development, Vite's features documentation (https://vite.dev/guide/features.html) describes the behaviour: importing a .css file "will inject its content to the page via a <style> tag with HMR support". That is why a style edit updates without a reload while a logic edit sometimes forces one, which connects directly to the bubbling mechanism in Question 12.
Production is a different pipeline, and the relevant default is build.cssCodeSplit, which defaults to true (https://vite.dev/config/build-options.html). Enabled, "CSS imported in async JS chunks will be preserved as chunks and fetched together when the chunk is fetched", so a lazily loaded route brings its own stylesheet with it. Disabled, "All CSS in the entire project will be extracted into a single CSS file".
That is a real trade rather than a right answer. One file is one request and compresses better across the whole corpus, but every visitor pays for every route's styles and one rule change invalidates all of it. Split files mean a returning visitor re-downloads only the part that changed, at the cost of more requests and the risk of a stylesheet arriving after the chunk that needs it.
Two details worth carrying. .module.css is the scoping convention: importing one "will return the corresponding module object" of generated class names, rather than relying on globally unique selectors. And assets referenced from CSS follow build.assetsInlineLimit, which defaults to 4096 bytes, so small files become inline data URIs and larger ones stay separate requests. Both are defaults somebody already chose for you.
The instinct to correct is that a library build is an application build with a different entry file. The goals are closer to opposite: an application bundle wants everything it needs in as few files as sensible, a library bundle wants to ship as little as possible and leave the rest to the consumer.
Vite's build documentation (https://vite.dev/guide/build.html) puts the critical instruction plainly: "Make sure to also externalize any dependencies that you do not want to bundle into your library, e.g. vue or react." The mechanism is build.lib plus an external list, paired with output.globals to name the global each external maps to in a UMD build, for example external: ['vue'] alongside globals: { vue: 'Vue' }.
Why externalizing matters is the Question 9 argument arriving from a different direction. Bundle React into a component library and every consumer who installs it gets a second copy of React alongside their own. React's own docs state the condition for hooks to work: the react import from application code has to resolve to the same module as the react import inside react-dom. Two copies break that, and they break it inside the consumer's application, where the cause is hard to see. Declaring React a peer dependency and externalizing it in the build is how you avoid shipping that problem to somebody else.
Output format is the other axis. Library mode emits es and umd for a single entry, and es and cjs when there are multiple entries, configurable through build.lib.formats. Which formats you ship decides who can consume the package, and it pairs directly with the exports map from Question 11: the formats are the files, and exports is what tells Node which file to hand to import versus require.
Version claims age fast, so here is what was true on 24 September 2026, with the reasoning that outlives the numbers.
Vite 8.3.x, current as of 24 September 2026, with 8.0.0 stable on 12 March 2026. Vite 8 replaced Rollup with Rolldown for production bundling and moved dependency pre-bundling from esbuild to Rolldown, giving Vite a unified bundling foundation. JavaScript and TypeScript transforms moved from esbuild to Oxc, and Oxc also became the default JavaScript minifier. That is visible in the package manifest, not just the announcement: Vite 8.3.x lists rolldown as a direct dependency at ~1.2.6, has no rollup or esbuild dependency, and keeps esbuild only as an optional peer. A nuance worth knowing: Vite 8.0.0 shipped depending on rolldown@1.0.0-rc.9, a release candidate published on 11 March 2026, and Rolldown 1.0.0 did not land until 7 May 2026, about two months after Vite 8 went stable. Vite's own release post (https://vite.dev/blog/announcing-vite8) attributes a production build time drop "from 46s to 6s" to Linear.
webpack 5.111.1, published 18 September 2026, on a steady cadence of roughly weekly releases. The honest framing comes from webpack's own 2026 roadmap (https://webpack.js.org/blog/2026-02-04-roadmap-2026/), published 4 February 2026, which says webpack "continues to be a stable and well-supported choice" and, in the same document, that "Open source projects like webpack are maintained primarily by volunteers, and there isn't a full-time engineering team dedicated to the project." Both halves are true and neither is a reason to call it finished. The roadmap says webpack 6 will make native CSS support non-experimental and add HTML as an entry point. It gives no release date, and neither should you.
esbuild 0.28.2, published 8 August 2026, still pre-1.0. Its FAQ (https://esbuild.github.io/faq/) describes it as "a late-stage beta", notes "This tool is primarily built by me", and puts hot module reloading, module federation and TypeScript type checking out of scope, with code splitting described as "currently pretty primitive". That is the clearest available explanation of why Vite never used esbuild for production builds and why Rolldown was built.
Turbopack became the default bundler for both next dev and next build in Next.js 16 (https://nextjs.org/blog/next-16), released 21 October 2025. You can opt out with next build --webpack.
Rollup 4.63.5 remains a common default for bundling libraries even after being displaced from Vite. Rspack 2.0 (https://rspack.rs/blog/announcing-2-0), released 22 April 2026, positions itself as compatible with the webpack ecosystem rather than as a drop-in clone; its own announcement reports roughly 10% faster than Rspack 1.7 and up to 100% faster than 1.0, and a @rspack/dev-server rewrite that cut dependencies from 192 to 1 and install size from 15 MB to 1.4 MB. Parcel 2.16.4 last shipped on 2 February 2026 and is best described as a low-configuration option rather than a dead one. Bun's bundler (https://bun.com/docs/bundler) targets "browser" by default, tree-shakes by default, and defaults to "esm" output with "experimental support for cjs and iife".
One cross-cutting fact that keeps coming up: require() of an ES module (https://nodejs.org/api/modules.html) is unflagged in Node.js 20.19.0, 22.12.0 and 23.0.0 and later. The remaining hard limit is top-level await, which throws ERR_REQUIRE_ASYNC_MODULE because require() has to be synchronous.
Useful things to demonstrate, based on what the underlying mechanisms actually reward:
optimization.runtimeChunk: 'single' or cache defaulting to false in production.Things that read weaker, and why:
moduleIds: 'deterministic' matters less than explaining what it protects.Reading about a pipeline is not the same as having debugged one. The fastest way to make frontend build tools interview questions concrete is to break a build on purpose and watch what happens.
Three exercises that take under an hour each:
[contenthash] to a webpack output filename, build twice with no source changes, and confirm the hash is stable. Then add one unused import and watch which chunks change. Then add runtimeChunk: 'single' and moduleIds: 'deterministic' and compare.sideEffects: false in a package that imports a CSS file, build for production, and find the missing styles.optimizeDeps.include and count again.For the interview itself, the pattern that transfers is the same across every topic: explain the mechanism, name the specific option, state the condition. GreatFrontEnd's JavaScript interview questions (https://www.greatfrontend.com/questions/js) give you the module semantics underneath all of this, including how ESM import and export bindings behave, which is the foundation tree shaking depends on.
Sixteen questions, and not one of them is actually answered by reciting configuration. They all come back to the same handful of ideas: that development and production do different work, that static module structure is what makes optimisation possible, that caching works at several layers and each one can be defeated separately, and that every default in a build tool is a trade someone already made for you.
If you prepare frontend build tools interview questions by memorising flags, the first follow-up question will find the edge of it. If you prepare by being able to explain why a build behaves the way it does, the flags come back on their own, and so does the ability to say "I would check the docs for that option, here is what I would be checking for." That answer is fine. It is what the job looks like.
Work through the full set of formats in GreatFrontEnd's Front End Interview Playbook , built by engineers who have run these loops from the other side of the table.
Learn how to implement Code Splitting and Lazy Loading in React and it's importance.
Prepare for web performance interview questions with Core Web Vitals, rendering, JavaScript cost, loading strategy, diagnostics, and model answers.