Skip to content

Top 32 Vite Interview Questions and Answers [2026]

November 8, 2026 27 min read
Vite interview guide by Second Talent
TL;DR: Vite interviews in 2026 test whether a candidate understands what changed when Rolldown and Oxc replaced esbuild and Rollup, and whether they can debug a plugin, an HMR boundary or an SSR setup without guessing. Candidates should know Vite 8 (stable since March 12, 2026, now 8.3.6 from November 5, 2026), Rolldown 1.x, the Environment API in its release candidate phase, and the Vite 8 migration traps around CommonJS interop and minification.

Vite 8 ships with one bundler instead of two: Rolldown now handles dependency pre-bundling in dev and the production build, and Oxc handles TypeScript and JSX transforms and JavaScript minification.

Linear reported production builds dropping from 46 seconds to 6 in the Vite 8 announcement, and the current release is Vite 8.3.6.

The questions below are for senior frontend, full-stack and tooling engineers, with four find-the-bug rounds on config, plugins, HMR and library builds.

Key takeaways
  1. 1Vite 8 raised the default browser targets to Chrome 111, Edge 111, Firefox 114 and Safari 16.4, up from 107, 107, 104 and 16.0 in Vite 7.
  2. 2JavaScript minification now uses the Oxc Minifier and CSS minification uses Lightning CSS; esbuild is an optional dependency.
  3. 3The object form of manualChunks is removed in Vite 8 and the function form is deprecated in favor of Rolldown's codeSplitting.
  4. 4The Environment API is in its release candidate phase, and the docs still say they do not recommend that apps switch to it yet.

Core Concepts and the Rolldown Switch

1. Why does the Vite dev server start faster than a bundler-based dev server?

Vite serves source files to the browser as native ES modules and transforms each one on request, so startup cost does not grow with the size of the app.

A bundler-based dev server has to crawl and bundle the whole module graph before it can serve the first page.

Vite splits the work in two: dependencies in node_modules are pre-bundled once and cached, and application source is transformed lazily when the browser asks for it.

A strong candidate names the cost of this model.

A page that imports hundreds of small source modules makes hundreds of requests on a cold load, which is why Vite has server.warmup for hot files and why the team built the experimental bundled dev mode (question 30).

A weak answer stops at "it uses esbuild", which has been out of date since Vite 8.

2. What does dependency pre-bundling do, and when does it re-run?

Pre-bundling converts CommonJS and UMD packages to ESM so the browser can import them, and collapses packages with many internal modules into one file so the browser does not make hundreds of requests for one import.

In Vite 8 the optimizer runs on Rolldown, and optimizeDeps.esbuildOptions is deprecated and converted to optimizeDeps.rolldownOptions automatically.

The result is cached under node_modules/.vite. Vite re-runs it when the lockfile, patch folder or relevant config fields change, and vite --force forces a rebuild. Candidates should know the two manual levers:

  • optimizeDeps.include for dependencies the import scanner misses, such as packages imported only from code a plugin generates, which otherwise trigger a re-optimize and a full page reload on first use.
  • optimizeDeps.exclude for ESM packages that must stay unbundled, for example a linked workspace package you edit alongside the app.

The Environment API docs add a detail many candidates miss: optimizeDeps stays a top-level option and applies only to the client environment.

3. What changed when Vite 8 moved to Rolldown?

Before Vite 8, dev used esbuild for dependency pre-bundling and transforms, and production used Rollup. Two tools meant two sets of behavior, so a bug could appear in the build that never showed up in dev.

Vite 8 uses Rolldown, a Rust bundler built by VoidZero with a Rollup-compatible plugin API, for both. The Vite team claims Rolldown is 10 to 30 times faster than Rollup in its benchmarks.

The practical changes an interviewer should hear:

  • build.rollupOptions is deprecated in favor of build.rolldownOptions, and worker.rollupOptions in favor of worker.rolldownOptions.
  • Most Rollup plugins work unchanged, but a few hooks are unsupported (question 16).
  • output.format values 'system' and 'amd' are no longer supported.
  • Vite 8 is about 15 MB larger to install than Vite 7, mostly from Lightning CSS becoming a regular dependency and from the Rolldown binary.

4. What does Oxc do in a Vite 8 project, and what does it not do?

Oxc transforms .ts, .jsx and .tsx files and minifies JavaScript in production builds.

The top-level esbuild config option is deprecated and converted to the new oxc option, with esbuild.jsx, esbuild.define and esbuild.jsxInject mapping to their oxc.* equivalents. Setting oxc: false disables the transform.

What Oxc does not do is type checking. Vite strips types per file and never runs the TypeScript compiler, so a type error does not fail vite build. A strong candidate runs tsc --noEmit (or vue-tsc) in CI or in a parallel process.

They also know two Oxc gaps from the migration guide: Oxc does not lower native decorators, and esbuild's property mangling options have no equivalent.

5. How do environment variables and modes work, and what is the security trap?

Vite loads .env, .env.local, .env.[mode] and .env.[mode].local, and exposes to client code only variables that start with envPrefix, which defaults to VITE_.

They appear on import.meta.env alongside built-ins such as MODE, DEV, PROD, SSR and BASE_URL, and they are replaced statically at build time.

The trap: anything exposed to the client ends up as plain text in the shipped JavaScript. A VITE_STRIPE_SECRET is public the moment it is referenced.

A strong candidate keeps secrets server-side, treats the prefix as a publishing decision, and knows that loadEnv(mode, process.cwd(), '') in the config file loads every variable for use in the config without exposing them.

The env and mode docs also note that loadEnv merges values already set in process.env.

Configuration

6. When do you export a function from vite.config instead of an object?

When the config depends on the command, the mode or the SSR flag. defineConfig accepts a function that receives command ('serve' or 'build'), mode, isSsrBuild and isPreview, and it can be async.

import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig(({ command, mode }) => {
  const env = loadEnv(mode, process.cwd(), '')
  return {
    plugins: [react()],
    define: {
      __APP_VERSION__: JSON.stringify(env.APP_VERSION),
    },
    build: {
      sourcemap: command === 'build' && mode === 'staging',
    },
  }
})

A weak answer reads process.env.NODE_ENV at the top of the config to branch on mode. That value is not set from .env files when the config is evaluated, so branches written that way pick the wrong path in CI.

A strong candidate uses the mode argument and loadEnv.

7. How do TypeScript path aliases work in Vite 8?

Vite 8 can read compilerOptions.paths from tsconfig itself. Set resolve.tsconfigPaths: true; the default is false because resolution has a small performance cost.

Vite 8.3 added a top-level tsconfig option that points Vite at a specific TypeScript config file, resolved from the project root, which helps in monorepos where the root tsconfig.json is a solution file with only references.

Before Vite 8 the standard answer was a plugin such as vite-tsconfig-paths or duplicating each alias in resolve.alias. Candidates who still maintain both lists should be asked how they keep them in sync.

The strong answer: pick one source of truth, and remember that tests run through Vitest pick up the same Vite resolution, so the aliases work there too.

8. What does this config produce in the browser bundle?

export default defineConfig({
  define: {
    __API_URL__: 'https://api.example.com',
    __BUILD_TIME__: Date.now(),
    'process.env': process.env,
  },
})

Answer: three different problems. define replaces identifiers with the value as raw code, and the shared options reference says the value must be a string containing a JSON-serializable value.

  • __API_URL__ is inserted as https://api.example.com without quotes, which is not a valid expression. Wrap it in JSON.stringify.
  • __BUILD_TIME__ is a number, which serializes to a valid literal, but it should still go through JSON.stringify for consistency with the documented contract.
  • 'process.env': process.env inlines every environment variable on the build machine, including CI tokens, into any file that reads process.env. That is a secret leak.

A senior candidate also mentions that Vite 8 changed define so object values no longer share a reference: each replaced site gets its own copy, so mutating one does not affect another.

9. How do you control chunking in a Vite 8 build?

Through Rolldown's build.rolldownOptions.output.codeSplitting option.

Vite 8 removed the object form of output.manualChunks and deprecated the function form. codeSplitting takes a list of groups, each with a name, a test pattern and optional priority, minSize, maxSize and minShareCount, documented in the Rolldown reference.

export default defineConfig({
  build: {
    rolldownOptions: {
      output: {
        codeSplitting: {
          groups: [
            { name: 'react', test: /node_modules[\\/](react|react-dom|scheduler)[\\/]/, priority: 20 },
            { name: 'vendor', test: /node_modules/, priority: 10, minSize: 20000 },
          ],
        },
      },
    },
  },
})

The tradeoff question to follow with: one large vendor chunk caches well across deploys but makes the first visit download code that most routes never use.

A strong candidate measures with a bundle visualizer or Vite Devtools before splitting, and keeps route-level splitting to dynamic import() rather than config.

10. What does the default build target mean, and when do you change it?

Since Vite 7 the default build.target is 'baseline-widely-available': browsers that support features in the Baseline Widely Available set as of a date fixed for each major release.

For Vite 8 that is Chrome 111, Edge 111, Firefox 114 and Safari 16.4, per the build guide. Vite lowers syntax to that target but does not add polyfills.

Change it when analytics show real users on older browsers, typically older iOS Safari on devices that cannot upgrade. The lowest accepted target is es2015, and passing two versions of the same browser now errors.

Candidates who need legacy browsers should mention @vitejs/plugin-legacy and its limit in Vite 8: transforming to ES5 or below is no longer supported.

11. Find the bug: a monorepo dev server that cannot load a shared package.

// apps/web/vite.config.ts
export default defineConfig({
  server: {
    fs: {
      allow: ['src'],
    },
  },
  optimizeDeps: {
    include: ['@acme/ui'],
  },
})

Bug: setting server.fs.allow turns off Vite's workspace root detection, so the list now permits only src. Files from packages/ui and from the hoisted root node_modules fall outside the allow list, and the dev server refuses to serve them.

The server options reference spells out that setting the option disables the automatic detection.

Fix: allow the workspace root, for example with searchForWorkspaceRoot(process.cwd()) plus any extra folders.

The second issue: a linked workspace package that you edit should usually go in optimizeDeps.exclude, not include, so changes reach the browser without a re-optimize.

A senior candidate also says never to set server.fs.strict: false on a dev server reachable from the network, because the server.fs.deny defaults (.env, .env.*, certificate and key files, .npmrc, .yarnrc.yml and .git) exist for a reason.

Plugins

12. In what order do Vite plugins run, and how do you control it?

Per the plugin API docs, the order is: alias, user plugins with enforce: 'pre', Vite core plugins, user plugins without enforce, Vite build plugins, user plugins with enforce: 'post', then Vite post-build plugins such as minify and manifest.

Inside one plugin, each hook can also take an order of 'pre' or 'post'.

The apply option limits a plugin to 'serve' or 'build', or takes a function for finer conditions such as "build but not SSR".

A strong candidate explains why order matters with an example: a plugin that transforms .vue files must run before @vitejs/plugin-vue compiles them, so it needs enforce: 'pre'; a plugin that rewrites final JavaScript runs after core transforms with enforce: 'post'.

13. How do you write a virtual module?

Resolve a prefixed ID to a private ID that starts with \0, then return generated code from load. The virtual: prefix is the user-facing convention, and the \0 prefix tells other plugins and Rolldown not to treat the ID as a file path.

export default function buildInfo() {
  const virtualId = 'virtual:build-info'
  const resolvedId = '\0' + virtualId

  return {
    name: 'build-info',
    resolveId(id) {
      if (id === virtualId) return resolvedId
    },
    load(id) {
      if (id === resolvedId) {
        return `export const builtAt = ${JSON.stringify(new Date().toISOString())}`
      }
    },
  }
}

In dev, Vite encodes \0 IDs as /@id/__x00__ URLs in the browser and decodes them before the hooks see them.

A candidate who has written one for TypeScript users also adds a declare module 'virtual:build-info' declaration, otherwise every import is a type error.

14. What are hook filters, and why do they matter more with Rolldown?

A hook filter declares, as data, which module IDs or code a hook cares about, so the bundler can skip calling the hook for other modules.

With Rolldown, the bundler runs in Rust and plugins run in JavaScript, so every hook call crosses that boundary. A transform hook that checks id.endsWith('.md') inside its body still gets called for every module in the graph.

import { exactRegex } from '@rolldown/pluginutils'

export default function markdown() {
  return {
    name: 'markdown',
    resolveId: {
      filter: { id: exactRegex('virtual:toc') },
      handler() { return '\0virtual:toc' },
    },
    transform: {
      filter: { id: /\.md$/ },
      handler(code, id) {
        return { code: compileMarkdown(code), moduleType: 'js' }
      },
    },
  }
}

Vite 6.3.0 and later and Rollup 4.38.0 and later support the syntax, so a plugin can adopt it without dropping older users, though the docs advise repeating the check in the handler for versions without filter support.

The moduleType: 'js' return value is the Vite 8 part: Rolldown infers module type from the file extension, so a plugin that turns a non-JS file into JavaScript must say so.

15. Which hooks are Vite-specific, and when does each run?

The Vite-only hooks are config, configResolved, configureServer, configurePreviewServer, transformIndexHtml, handleHotUpdate and the per-environment hotUpdate. Vite 8.3 added closeServer and closePreviewServer hooks for cleanup.

The universal Rollup-style hooks run in dev too, with differences candidates should know:

  • options and buildStart run once when the dev server starts.
  • resolveId, load and transform run per module request.
  • moduleParsed never runs in dev, because Vite skips full AST parses there.
  • Output generation hooks, except closeBundle, run only in builds.

A detail from configureServer that separates people who have used it: middleware added directly runs before Vite's internal middleware.

To run after it, for example to serve a custom fallback page, return a function from the hook and register the middleware inside it.

16. What breaks when you move a Rollup plugin to Vite 8?

Mostly edge hooks and bundle mutation. The Vite 8 migration guide lists what Rolldown does not support:

  • The hooks shouldTransformCachedModule, resolveImportMeta, renderDynamicImport and resolveFileUrl.
  • Assigning to bundle[fileName] in generateBundle; use this.emitFile().
  • structuredClone(bundle), which fails; clone with structuredClone({ ...bundle }).
  • Parallel hooks now run sequentially, and comments are removed before renderChunk instead of after.
  • Extglobs in include and exclude patterns.

Plugins that call transformWithEsbuild need esbuild installed, since it is now optional; the replacement is transformWithOxc. parseAst and parseAstAsync are deprecated in favor of parseSync and parse.

A strong candidate checks the plugin on registry.vite.dev and in the Vite 8 ecosystem CI results before forking it.

17. Find the bug: a plugin that injects a banner.

export default function banner() {
  return {
    name: 'banner',
    transform(code, id) {
      if (id.includes('src/')) {
        return `/* (c) Acme */\n` + code
      }
    },
  }
}

Bugs:

  • Broken source maps. The transform returns a string with no map, so every line in a matched file is off by one in DevTools and in error reports. Use magic-string and return { code: s.toString(), map: s.generateMap({ hires: true }) }.
  • Wrong match. id.includes('src/') also matches node_modules/some-lib/src/ and matches CSS and virtual modules. Use a filter with a specific regex and skip IDs that start with \0.
  • Wrong layer. A license banner belongs on output chunks, not on every module, and Vite 8 removes comments before renderChunk. Rolldown's output.banner option, set through build.rolldownOptions, is the place for it. The migration guide also notes that esbuild.banner no longer exists.

A weak answer finds the path check and stops. The source map issue is the one that costs a team hours later.

Hot Module Replacement

18. How does Vite HMR decide what to update?

When a file changes, Vite walks up the module graph from the changed module until it finds an HMR boundary, a module that accepts updates for itself or for the changed dependency.

If it finds boundaries on every path, it sends the browser an update for those modules; if any path reaches the root without one, it does a full page reload.

Framework plugins such as @vitejs/plugin-react and @vitejs/plugin-vue make components into boundaries for you.

The HMR API is on import.meta.hot: accept, dispose, prune, invalidate, data, on and send. Calls must sit inside an if (import.meta.hot) guard so they are removed from production builds.

A strong candidate can explain why editing a file that exports both a component and a constant forces a reload in React: React Refresh only accepts modules that export components alone.

19. Find the bugs: a module that keeps a timer across hot updates.

// src/clock.js
const timer = setInterval(tick, 1000)

if (import.meta.hot) {
  import.meta.hot.accept()
  import.meta.hot.data = { startedAt: Date.now() }
}

Bugs:

  • No dispose. Every edit runs the module again and starts another interval, so after ten saves tick runs ten times a second. Register import.meta.hot.dispose(() => clearInterval(timer)).
  • Reassigning data. The HMR docs state that reassigning import.meta.hot.data is not supported. Mutate its properties instead, and read the old value before writing: import.meta.hot.data.startedAt ??= Date.now().
const timer = setInterval(tick, 1000)

if (import.meta.hot) {
  import.meta.hot.data.startedAt ??= Date.now()
  import.meta.hot.dispose(() => clearInterval(timer))
  import.meta.hot.accept()
}

A follow-up worth asking: Vite 8 removed support for passing a URL to import.meta.hot.accept, so dependency accepts must pass a module ID.

20. When do you use hotUpdate instead of handleHotUpdate?

Use hotUpdate in plugins that need to work with more than the browser, such as an SSR or edge environment. handleHotUpdate sees one module graph; hotUpdate runs once per environment with this.environment set, receives type ('create', 'update' or 'delete'), file, modules and a read() helper, and returns the modules to update.

hotUpdate({ file, modules, timestamp }) {
  if (this.environment.name !== 'client') return
  if (!file.endsWith('.i18n.json')) return

  const invalidated = new Set()
  for (const mod of modules) {
    this.environment.moduleGraph.invalidateModule(mod, invalidated, timestamp, true)
  }
  this.environment.hot.send({ type: 'full-reload' })
  return []
}

Returning an empty array tells Vite the plugin handled the update.

A candidate who has built a custom HMR flow will also mention environment.hot.on and client.send for custom events between a plugin and running code, documented in the Environment API plugin guide.

21. A one-line change triggers a full reload instead of HMR. How do you debug it?

Start the server with vite --debug hmr (or DEBUG=vite:hmr) and read which module the update propagated to and why it stopped. The usual causes:

  • A module on the path has no boundary, such as a plain utility imported by main.ts.
  • A React file exports a non-component value next to a component.
  • A circular import puts the root module inside the update chain.
  • A dependency was discovered late and the optimizer re-ran, which forces a reload; add it to optimizeDeps.include.
  • A boundary called import.meta.hot.invalidate() because it could not handle the change.

If HMR fails behind a reverse proxy or in a container, the issue is usually the WebSocket.

Vite 8.1 moved the connection settings (protocol, host, port, path, clientPort, timeout, server) from server.hmr to server.ws; the old fields are deprecated and kept in sync.

SSR and the Environment API

22. How do you run Vite in middleware mode for a custom SSR server?

Create the Vite server with server.middlewareMode: true and appType: 'custom', mount vite.middlewares on your Node server, and per request transform the HTML template and load the server entry through Vite so it gets HMR and source transforms.

import fs from 'node:fs/promises'
import express from 'express'
import { createServer } from 'vite'

const app = express()
const vite = await createServer({
  server: { middlewareMode: true },
  appType: 'custom',
})
app.use(vite.middlewares)

app.use('*all', async (req, res, next) => {
  try {
    const url = req.originalUrl
    const raw = await fs.readFile('index.html', 'utf-8')
    const template = await vite.transformIndexHtml(url, raw)
    const { render } = await vite.ssrLoadModule('/src/entry-server.tsx')
    const html = template.replace('<!--app-html-->', await render(url))
    res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
  } catch (e) {
    vite.ssrFixStacktrace(e)
    next(e)
  }
})

app.listen(5173)

Candidates should mention ssrFixStacktrace, which maps errors back to source, and the production split: vite build for the client, a second build with --ssr src/entry-server.tsx for the server, and static serving of the client output.

The SSR guide has the full example.

23. What problem does the Environment API solve?

Vite used to model two environments, the browser and Node SSR, with an ssr boolean threaded through hooks. Apps now run code in several runtimes at once: browser, Node, edge workers such as Cloudflare's workerd, and React Server Components.

The Environment API lets a config declare any number of named environments, each with its own resolve, define, dev and build options and its own module graph.

export default defineConfig({
  environments: {
    client: {},
    ssr: {},
    edge: {
      resolve: { noExternal: true, conditions: ['workerd', 'worker'] },
      build: { outDir: 'dist/edge' },
    },
  },
})

Environments inherit top-level options unless documented otherwise. In dev, client and ssr always exist; in a build, ssr exists only when configured.

Status matters for hiring: the docs call the API "generally in the release candidate phase" with some APIs still experimental, and say app authors should not switch yet.

It is aimed at framework and plugin authors, and the top-level ssr option will be deprecated once it is stable.

24. What replaces ssrLoadModule, and why?

The module runner. A runnable dev environment exposes environment.runner, and runner.import(url) loads a module through that environment's plugin pipeline and module graph.

The frameworks guide calls it the modern replacement for server.ssrLoadModule that lets frameworks enable HMR for server code.

import { createServer, isRunnableDevEnvironment } from 'vite'

const server = await createServer({ server: { middlewareMode: true }, appType: 'custom' })
const ssr = server.environments.ssr

if (isRunnableDevEnvironment(ssr)) {
  const { render } = await ssr.runner.import('/src/entry-server.tsx')
}

Not every environment is runnable from the Vite process. An edge environment may execute in a separate runtime and talk to Vite over a transport, which is why the guard exists.

A strong candidate explains that ssrLoadModule still works and that frameworks such as Nuxt, Astro and React Router handle this layer, so app teams rarely call either one directly.

25. How does vite build --app differ from a plain vite build?

A plain vite build builds the client environment.

When the builder option is set, even to {}, which is what vite build --app does, Vite builds the whole app: every configured environment, in an order a plugin or the config can control with builder.buildApp.

export default defineConfig({
  builder: {
    async buildApp(builder) {
      await builder.build(builder.environments.client)
      await builder.build(builder.environments.ssr)
    },
  },
})

The order matters when the server build needs the client manifest to inject asset links. Vite 7 added a buildApp plugin hook so framework plugins can coordinate this without owning the user's config.

Programmatically, createBuilder() followed by builder.buildApp() does the same.

26. How should a plugin keep state when several environments run at once?

Key it by environment. In dev, one plugin instance serves all environments, so a module-level Map or Set mixes client and server data. The docs recommend a Map<Environment, State> keyed by this.environment.

function countModules() {
  const state = new Map()
  return {
    name: 'count-modules',
    buildStart() {
      state.set(this.environment, { count: 0 })
    },
    transform(code, id) {
      state.get(this.environment).count++
    },
    buildEnd() {
      const { count } = state.get(this.environment)
      console.log(`${this.environment.name}: ${count} modules`)
    },
  }
}

Two related tools a candidate should know. applyToEnvironment(environment) lets a plugin opt in or out per environment, or return a separate plugin instance for each one; Vite 8.3 warns when that returned plugin uses unsupported hooks. sharedDuringBuild: true opts a plugin into one shared instance across environments during builds too, where the default is one instance per environment.

Builds, Library Mode and Deployment

27. How do you configure library mode, and what does it output?

Set build.lib with an entry, a global name for UMD and IIFE output, and a fileName, and list peer dependencies as externals. With one entry, the default formats are es and umd; with several entries they are es and cjs.

import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'

const __dirname = dirname(fileURLToPath(import.meta.url))

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'lib/main.ts'),
      name: 'AcmeUi',
      fileName: 'acme-ui',
      cssFileName: 'style',
    },
    rolldownOptions: {
      external: ['vue'],
      output: { globals: { vue: 'Vue' } },
    },
  },
})

With "type": "module" in package.json, that produces dist/acme-ui.js and dist/acme-ui.umd.cjs, which the package's exports map should point to. Imported CSS is merged into one file, named by build.lib.cssFileName.

The build guide adds two notes: process.env.* is not replaced in library mode, and for libraries that do not target the browser, tsdown or Rolldown directly fit better.

28. Find the bug: a React component library that breaks consumer apps.

// vite.config.ts in the library
export default defineConfig({
  plugins: [react()],
  build: {
    lib: { entry: 'src/index.ts', formats: ['es'], fileName: 'index' },
  },
})

// package.json
{
  "name": "@acme/ui",
  "type": "module",
  "main": "./dist/index.js",
  "dependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" }
}

Bug: React is not externalized, so the library bundles its own copy.

Consumer apps then have two Reacts, and hooks inside library components fail with "Invalid hook call", because the hook dispatcher belongs to the copy the app renders with.

Listing React under dependencies makes it worse, because package managers may install a second copy too.

Fix: move react, react-dom and react/jsx-runtime to peerDependencies (and devDependencies for local work), and externalize them with build.rolldownOptions.external, matching subpaths with a regex such as /^react(-dom)?(\/.*)?$/.

Add an exports map and a types entry. A strong candidate checks the result by opening dist/index.js and confirming it contains import ... from "react" rather than React's source.

29. Users see blank pages right after a deploy. What happened, and how do you handle it?

Users with the old HTML loaded tried to fetch a hashed chunk that the new deploy deleted, so the dynamic import failed.

Vite fires a vite:preloadError event on window when a dynamic import fails, with the original error in event.payload, and calling event.preventDefault() stops it from being thrown.

window.addEventListener('vite:preloadError', (event) => {
  event.preventDefault()
  window.location.reload()
})

A reload loop guard is the detail a senior candidate adds, for example a timestamp in sessionStorage.

The fuller answer covers deploys too: serve the HTML with Cache-Control: no-cache, keep at least the previous release's assets on the CDN for a while, and use long-lived immutable caching only for hashed files.

Vite 8.1's experimental chunk import maps help a related problem: a change in one chunk no longer re-hashes every chunk that imports it, so more files stay cached across deploys.

30. What is bundled dev mode, and when would you try it?

Bundled dev mode, previously called Full Bundle Mode, bundles modules during development instead of serving each one as a separate request. It is experimental and enabled with --experimental-bundle or experimental.bundledDev: true.

The Vite 8.1 announcement reports about 15 times faster startup and 10 times faster full reloads on a React test app with 10,000 components, and Linear reported 10 times fewer network requests.

It targets the weakness from question 1: large apps where unbundled ESM means thousands of requests on a cold load.

The tradeoffs a candidate should name are that third-party plugins may not work yet, since the mode focuses on browser-side and basic plugins, and that behavior is still changing between minor releases.

A sensible answer is to trial it on a branch of a large app, measure cold start and reload times, and keep it off in the shared default config until it leaves experimental status.

What Changed Recently

Jun 24, 2025
Vite 7 requires Node 20.19+ or 22.12+ and defaults to 'baseline-widely-available'
Dec 3, 2025
Vite 8 beta ships with Rolldown fully integrated
Mar 12, 2026
Vite 8 and @vitejs/plugin-react 6: Rolldown and Oxc by default, Babel dropped from the React plugin
May 7, 2026
Rolldown 1.0 stable
Jun 23, 2026
Vite 8.1: WASM ESM integration, chunk import maps, bundled dev mode, server.ws
Jul 30, 2026
Vite 8.2 adds a top-level input option
Sep 10, 2026
Vite 8.3 adds a top-level tsconfig option and dev server Devtools integration
Nov 5, 2026
Vite 8.3.6 is the latest patch release

31. What are the riskiest parts of a Vite 7 to Vite 8 upgrade?

The changes that alter runtime behavior without a config error. From the migration guide, in rough order of how often they bite:

  • CommonJS default imports are now handled consistently, which the guide warns may break existing code. legacy.inconsistentCjsInterop: true restores the old behavior temporarily.
  • Resolution no longer sniffs the format of browser and module fields and always follows resolve.mainFields order.
  • Minifiers changed to Oxc and Lightning CSS, so output and occasional edge-case bugs differ. build.minify: 'esbuild' (deprecated, needs esbuild installed) and build.cssMinify: 'esbuild' switch back.
  • import.meta.url is no longer polyfilled in UMD and IIFE output, so it is undefined there by default.
  • require calls to externalized modules are preserved instead of converted to imports; esmExternalRequirePlugin converts them.
  • build() in the JavaScript API now throws a BundleError with an errors array.
  • build.commonjsOptions is a no-op.

The upgrade path the Vite team recommends for large projects is to switch to the rolldown-vite package on Vite 7 first, then move to Vite 8, so bundler issues and Vite API issues show up separately.

Node requirements did not change: 20.19+ or 22.12+.

32. What did Vite 8.1 through 8.3 add that a candidate should know?

Per the Vite changelog:

  • 8.1 (June 23, 2026): direct .wasm imports through WASM ESM integration, upstreamed from vite-plugin-wasm; experimental chunk import maps; bundled dev mode under its new name; import.meta.glob with caseSensitive: false; html.additionalAssetSources; a longer default server.fs.deny list; and the server.hmr WebSocket fields moved to server.ws.
  • 8.2 (July 30, 2026): a top-level input option for application entry points, also accepted by server.fs.allow, and HMR for worker files in bundled dev mode.
  • 8.3 (September 10, 2026): a top-level tsconfig option, closeServer and closePreviewServer hooks, Rolldown watch options in server.watch, minified inline style tags, and Vite Devtools integration with the dev server.

Vite 8.0 itself added server.forwardConsole, which sends browser console output and errors to the terminal.

Its default is on when Vite detects an AI coding agent and off otherwise, which matters for teams that run agents against their dev server.

Vite 8.1 also said the team is considering Lightning CSS as the default CSS transformer in the next major release; today css.transformer defaults to 'postcss'.

Signs of a Strong Answer

  • They describe Vite 8 as Rolldown plus Oxc for dev and build, and use rolldownOptions and codeSplitting in config without prompting.
  • They run type checking separately from vite build and can say why the build passes with type errors.
  • They treat every VITE_ variable and every define value as public, and wrap define values in JSON.stringify.
  • They return source maps from transforms and use hook filters instead of checking IDs inside the handler.
  • They can trace an HMR update to its boundary with --debug hmr and clean up side effects in dispose.
  • They know the Environment API is a release candidate aimed at framework authors, and can still explain runner.import and builder.buildApp.

Hiring Vite Engineers

Engineers who have migrated a large app to Vite 8, written a plugin with correct source maps, or run SSR through the module runner are hard to spot from a resume that lists "Vite" next to ten other tools.

Second Talent helps US startups hire vetted AI-Native Engineers from Asia, including front-end developers, React developers and Vue developers, screened with questions like these.

For testing on the same toolchain, see our Vitest interview guide, and for the language layer, the TypeScript interview guide.

Rates run $23 to $39 USD an hour, billed monthly, with matches in 24 hours and a 90-day one-time replacement guarantee. Compare salaries on the cost to hire a frontend developer page, or tell us what you need.

Get developers who ship with AI agents.

Starts at $23/Hour.

Start Hiring

$0 cost until you hire