frontend

Vite 8 Build Fails After Upgrade: Causes and Fixes (2026)

September 3, 2026

Vite 8 Build Fails After Upgrade: Causes and Fixes (2026)

Vite 8 replaced Rollup and esbuild with Rolldown and Oxc, so builds that worked on Vite 7 can fail on behaviour changes rather than config errors. The documented causes include Lightning CSS minification, non-ESM top-level await, unified CommonJS interop, unlowered native decorators, and a short list of removed options.

TL;DR

Vite 8 shipped stable on March 12, 2026 and replaced both of Vite's bundlers — esbuild for development transforms, Rollup for production — with a single Rust bundler, Rolldown, plus the Oxc toolchain for transforms and minification.1 Vite ships a compatibility layer: build.rollupOptions is a still-working deprecated alias, and the esbuild and optimizeDeps.esbuildOptions options are converted to their Oxc and Rolldown equivalents automatically.23

What breaks instead is the long tail: two new default minifiers, a stricter top-level-await rule, a unified CommonJS default import rule, native decorators that Oxc does not lower, and a short list of genuinely removed options. This page walks each documented failure to its fix, verified against Vite 8.2.2.43

What you'll learn

  • Why the bundler swap, not your config, is the root cause of most Vite 8 build failures
  • Whether build.rollupOptions was renamed, removed, or neither — and what happens if you set both
  • Which options were removed outright, as opposed to deprecated, no-op, or unsupported by the new tools
  • How Lightning CSS minification silently changes or rejects your CSS, and four ways to opt out
  • What The "TopLevelAwait" is not supported with the "iife" output format means, and why worker.format is usually the cause
  • Why resolve.alias can stop working in a programmatic build() call, and the trap in the workaround
  • Why a default import from a CommonJS package now returns an object
  • Why native decorators stop compiling while your experimentalDecorators code keeps working
  • What the evidence actually says about Yarn PnP, in both directions
  • Why your bundle targets newer browsers than it did on Vite 7
  • Whether you still need esbuild installed
  • How to actually find the one plugin that is breaking your build
  • Why a patch release of Vite 8 can break a build that worked yesterday
  • Why it builds locally and fails in CI, which is a different problem from all of the above
  • The gradual-migration path, and a real rollback procedure
  • What the speed gain costs, and who measured it

Why does my build fail after upgrading to Vite 8?

Because Vite 8 changed bundlers, not because your config is wrong. Vite used esbuild for dev-time transforms, dependency pre-bundling and minification, and Rollup for production bundling; Vite 8 uses Rolldown for bundling and dependency optimization, and Oxc for JavaScript transforms and minification.3 Anything that depended on Rollup's or esbuild's specific behaviour — an output-format rule, a plugin hook, a minifier assumption — is now running through different code.

Vite's own framing is that this is "the most significant architectural change since Vite 2."1 That is a useful expectation to set: a failing build after the upgrade is usually a genuine behavioural difference, not a missing rename.

The practical consequence is that the fix rarely lives in a search for your error string. It lives in one of seven buckets:

BucketTypical symptomWhere it is covered below
Removed optionA setting that no longer applies, with no equivalent in placeWhich options were removed
New default minifierCSS rejected at build time, or visually broken output with no errorWhy your CSS breaks
Output-format constraintBuild error naming a file inside node_modulesTop-level await
Unsupported transformDecorators, or ES5 output, that used to compileDecorators
Resolution differences"failed to resolve import" for a path that used to workAliases, and patch-release regressions
Runtime-only changeGreen build, wrong behaviour in the browser or on the serverCommonJS interop
Environment, not codeBuilds locally, fails only in CIWhy it fails in CI

Work down that table before touching your config. Note the "New default minifier" and "Runtime-only change" rows especially: those two failures can produce no build error at all, which is why searching for an error string finds nothing.

Do I need to rename build.rollupOptions to build.rolldownOptions?

No. build.rollupOptions still works in Vite 8. The config reference documents it as "an alias of build.rolldownOptions" and marks it Deprecated, not removed.2 worker.rollupOptions is documented identically.5 Separately, optimizeDeps.esbuildOptions and the top-level esbuild option are converted: the guide publishes mapping tables and Vite applies them for you, though two entries are not mechanical — esbuild.banner and esbuild.footer map to "custom plugin using transform hook", and esbuildOptions.plugins is marked "(partial support)".3

This is worth stating plainly because a good deal of the 2026 write-ups about Vite 8 lead with "rollupOptions was renamed to rolldownOptions" as though a rename were the required first migration step. It is a recommended cleanup, not a fix.

Those write-ups are not inventing the word, either. Vite's own migration guide lists the option under "Other Related Deprecations" as "build.rollupOptions: renamed to build.rolldownOptions", while the configuration reference for the same version calls it an alias that is merely deprecated.32 Two pages in the same docs set, two framings. The config reference is the one that describes runtime behaviour.

The one thing that will genuinely bite you is setting both. If a config carries build.rollupOptions and build.rolldownOptions, the docs say nothing about precedence — but Vite's source does, in a comment above the line that implements it: "if both rollupOptions and rolldownOptions are present, ignore rollupOptions and use rolldownOptions".6 They are not merged. The rollupOptions object is discarded whole. That matters because the alias workaround later on this page asks you to add a build.rolldownOptions block — if you already have externals, chunking or output settings under build.rollupOptions, adding that block silently drops all of them. Migrate the whole object, not part of it.

There is a related warning that only fires for plugin-supplied config, not your own: "Both rollupOptions and rolldownOptions were specified by … plugin. rollupOptions specified by that plugin will be ignored."6

One nuance on deprecation warnings, because it is the opposite of what you would guess. Vite's runtime deprecation warning for the rollupOptions name is scoped in source to optimizeDeps and ssr.optimizeDeps only — build and worker are not in that set.6 So optimizeDeps.rollupOptions warns and build.rollupOptions does not. When a warning does fire and the option was set by a dependency rather than by you, Vite's message names the escape hatch: "Set VITE_DEPRECATION_TRACE=1 to see where it is called", which switches the log from console.warn to console.trace so you get the call site.6 That environment variable is not documented on vite.dev; it is visible in the source.

The other day-one move is inspecting what the compatibility layer produced. Note that this needs to be a registered plugin — a bare object assigned to a variable does nothing:

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    {
      name: 'log-config',
      configResolved(config) {
        console.dir(config.optimizeDeps.rolldownOptions, { depth: null })
        console.dir(config.oxc, { depth: null })
      },
    },
  ],
})

Both property paths come from the migration guide's own configResolved examples.3 Two caveats before you read the output: config.optimizeDeps.rolldownOptions will be undefined if you never set optimizeDeps.esbuildOptions and no plugin did either, which is normal rather than a bug; and console.dir with depth: null is deliberate, because console.log on a resolved config prints [Function] for exactly the entries you most want to see.

Which Vite 7 options were actually removed in Vite 8?

Fewer than the migration chatter suggests, and the guide sorts them into categories that are worth keeping distinct, because only the first will leave you with no equivalent at all.3

Genuinely removed.

  • build.rollupOptions.watch.chokidar — use build.rolldownOptions.watch.watcher.
  • The object form of output.manualChunks. The function form is deprecated but still present.
  • Passing a URL to import.meta.hot.accept — pass an id instead.

Not supported by Rolldown, which the guide lists separately under "Missing support by Rolldown": the system and amd output formats, and the shouldTransformCachedModule, resolveImportMeta, renderDynamicImport and resolveFileUrl hooks.3

Not supported by Oxc, where the option name survives but the capability does not: esbuild.supported, and esbuild's property-mangling options (mangleProps, reserveProps, mangleQuoted, mangleCache).3

Now a no-op — the option still parses and does nothing: build.commonjsOptions, and build.dynamicImportVarsOptions.warnOnError.3 This is the category to check first, because a no-op cannot announce itself. If you were relying on commonjsOptions to shape CJS handling, that behaviour is gone.

One item that is often filed as a removal but is really new input validation: passing the same browser with multiple versions of it to build.target now errors, where esbuild "selects the latest version of it, which was probably not what you intended."3

The manualChunks change is the one worth checking first, because the object form is the shape that "split your vendor bundle" guidance has taught for years:

// Vite 7 — the object form is no longer supported in Vite 8
build: {
  rollupOptions: {
    output: {
      manualChunks: { vendor: ['react', 'react-dom'] },
    },
  },
}

The replacement is Rolldown's codeSplitting, at build.rolldownOptions.output.codeSplitting. It is not a rename — the shape is different. A group's test is optional and accepts a string, a regular expression, or a predicate function over the module id, rather than a list of package names, so the translation is a rewrite rather than a copy:7

// Vite 8
build: {
  rolldownOptions: {
    output: {
      codeSplitting: {
        groups: [
          { name: 'vendor', test: /node_modules[\\/]react(-dom)?[\\/]/ },
        ],
      },
    },
  },
}

Two documented behaviours to expect from that option before you ship it. Rolldown's own guidance: "If you used manual code splitting with groups, rolldown will forcefully generate a runtime.js chunk to ensure that the runtime code is always executed before any other chunks", and "When a module is captured by a group, Rolldown will try to capture its dependencies recursively without considering constraints."7 Both change your chunk graph in ways a naïve translation will not predict, so diff the emitted chunk list rather than trusting the config to be equivalent.

Why does my CSS break or fail to minify in Vite 8?

Because build.cssMinify now defaults to 'lightningcss' instead of esbuild — with the one exception that it is false when build.minify is disabled for the client build.2 Lightning CSS is stricter about syntax it does not recognise and more opinionated about what it rewrites, and it produces two distinct failure modes.

The loud one is a build error on modern CSS. A tracked issue opened on March 17, 2026 — "Default CSS minifier in Vite@8 blocks some progressive modern CSS features" — reports the build failing because the @scope rule is rejected during minification, along with problems for ::scroll-marker, ::scroll-marker-group, ::scroll-button(), :target-current, and ::search-text.8 It is labelled as an upstream bug and was still open at the time of writing. The reporter's stated workaround: "Switching the CSS minifier back to esbuild resolves the issue."8

The quiet one is worse. A report from June 9, 2026, on Vite 8.0.16 with lightningcss 1.32.0 describes the unprefixed backdrop-filter declaration being dropped from the output, leaving only -webkit-backdrop-filter and breaking glass and blur effects — with no error at any point.9 It was closed as a duplicate of an earlier issue whose title states the precise trigger: "backdrop-filter before -webkit-backdrop-filter is dropped with cssMinify: 'lightningcss'".10 So the ordering of the two declarations is what matters, and that earlier issue — opened November 20, 2025 and also labelled an upstream bug — was still open at the time of writing.10 This one is not fixed; it is tracked.

You have four levers, from lightest to heaviest. The first three are worth trying before the last one, which is the only one that changes your whole CSS pipeline:

  1. Override the transitive Lightning CSS version. Both of these are upstream bugs in Lightning CSS rather than in Vite, so an overrides (npm), resolutions (Yarn) or pnpm.overrides entry pinning a version that behaves is the narrowest possible fix.
  2. Set build.cssTarget. When cssMinify is 'lightningcss', build.cssTarget takes precedence over css.lightningcss.targets for the minification step, which is the knob for syntax-lowering behaviour — including the case where your CSS bundle grew after upgrading.2
  3. Disable CSS minification entirely with build.cssMinify: false as a triage step, to confirm the minifier is the culprit before you change anything permanent.
  4. Switch the minifier back to esbuild, which reverts the whole CSS pipeline and is the option both issue reporters used:
// vite.config.js
export default defineConfig({
  build: { cssMinify: 'esbuild' },
})
npm add -D "esbuild@^0.27.0 || ^0.28.0"

The config reference states the dependency requirement directly: "esbuild must be installed when it is set to 'esbuild'."2 Pin it to the range Vite declares as its peer dependency rather than installing whatever is latest, or a future esbuild major will fail a strict install.4

The migration guide also notes that Lightning CSS "supports better syntax lowering and your CSS bundle size might increase slightly", so a somewhat larger CSS file after upgrading is expected — but if the increase is large, build.cssTarget is the lever, not resignation.3

What does The "TopLevelAwait" is not supported with the "iife" output format mean?

It means something in your bundle uses top-level await, and the output format cannot express it. The constraint is not specific to IIFE: Rolldown's rule is that "If your input contains TLA, it could only be bundled and emitted with esm format."11 Every non-ESM format is affected, so switching from IIFE to UMD or CJS will fail identically.

So the real question is where a non-ESM output format entered your build. There are three common answers, and only one of them is usually deliberate.

Workers. worker.format defaults to 'iife', and its only other allowed value is 'es'.5 A dependency with top-level await that is fine in your main bundle can therefore blow up the worker bundle alone. That makes this the cheapest thing to try:

// vite.config.js
export default defineConfig({
  worker: { format: 'es' },
})

Check your browser support before shipping it — module-worker support is why the default is still 'iife'.

Library builds. Vite's default build.lib.formats are "['es', 'umd'], or ['es', 'cjs'], if multiple entries are used" — so a library build emits a non-ESM bundle whether or not you asked for one.2 Expect the error to name whichever of those formats applies rather than "iife"; the constraint is the same, since Rolldown emits top-level await only in esm.11

Neither of the above. A report filed on April 23, 2026 is exactly this case: a plain vite build that succeeded on Vite 8.0.5 fails on 8.0.10, with the error pointing at node_modules/@novnc/novnc/lib/util/browser.js and no worker in the reproduction at all.12 Two things are worth taking from it. The file named in the error is a dependency, not your source, so searching your own code will waste time. And the failure moved between patch releases, which means "it worked last week" is not evidence that your config is fine — see the patch-release section below.

If the format is not yours to change — a library that must keep publishing umd or iife for <script>-tag consumers — the remaining options are to remove the top-level await from the dependency graph, or to build as ESM and downlevel afterwards. Pinning is a last resort and worth naming precisely: the cited report's last good version was 8.0.5, which is a long way behind 8.2.2.

Why did resolve.alias stop working in my programmatic build() call?

Because in at least one build path the alias never reaches Rolldown's resolver. The narrow, verified case is build() invoked from a Cypress preprocessor. A discussion opened on May 1, 2026 against Vite 8.0.10 reports it with the error Rolldown failed to resolve import "@/model/Alert.model", and the follow-up bug report filed on May 8 against 8.0.11 reproduces it as Rolldown failed to resolve import "@/foo.js".1314

Scope matters here, because the issue titles say "library mode" and the reproduction says something narrower. The reporter's own repro shows the same function succeeding when run as a standalone script and throwing only under the Cypress preprocessor.14 This is not "programmatic builds are broken," and it is not "library mode is broken."

The workaround is to declare the alias where Rolldown will see it as well:

import path from 'node:path'

await build({
  configFile: false,
  resolve: { alias: { '@': path.resolve('./src') } },
  build: {
    rolldownOptions: {
      resolve: { alias: { '@': path.resolve('./src') } },
    },
    lib: { entry: filePath, formats: ['es'] },
  },
})

The reporter states that moving resolve.alias under build.rolldownOptions "causes it to work in both cases."14 Note the absolute path: the reproduction resolves it with path.resolve rather than passing a relative string.

Two warnings before you copy this. First, if your config already has a build.rollupOptions block, adding build.rolldownOptions makes Vite ignore the rollupOptions object entirely rather than merging it — see the precedence rule above.6 Migrate everything across in the same edit. Second, the follow-up issue was closed as not planned, so this is not a bug awaiting a fix and the duplication may be permanent.14 Two other things the reporter tried — configFile: false alone, and the experimental native-plugin toggles — did not help.13

There is a related resolution change that produces similar-looking errors from a different cause. Vite used to sniff file contents when a package declared both browser and module fields, and would pick the ESM file for browsers. Vite 8 dropped that heuristic and always respects the order of resolve.mainFields.3 If a package that previously resolved to its ESM build now resolves to a UMD one, that is why, and the documented remedies are a resolve.alias entry or a package manager patch.3

Why does a default import from a CommonJS package now return an object?

Because Vite 8 made CommonJS default interop consistent, and consistency changes what some imports mean. Vite's troubleshooting page names the symptom exactly — "Default import unexpectedly returns an object" — and describes it as: "The default import returns the module.exports object for CJS modules, while you may expect it to return the module.exports.default value."15

That is worth stating carefully, because the intuitive guess is that the import becomes undefined. The documented failure runs the other way: you get the whole module.exports object where you expected one property off it. The symptom is usually x.default is not a function, or a component that renders as [object Object], from a build that succeeded.

The rule the migration guide states is that the default import is the importee's module.exports value if any of these hold:3

  • the importer is .mjs or .mts
  • the importer's closest package.json has "type": "module"
  • the importee's module.exports.__esModule is not true

Otherwise, the default import is module.exports.default.

Rolldown's own documentation, which the guide links for the full picture, lists a longer set of conditions than Vite's three — including two that apply only to dynamic imports, and one for the case where module.exports has no own default property at all.16 If Vite's three conditions do not explain what you are seeing, that page is where the remaining cases live.

Under Vite 7 the rules differed between dev and build, and in dev they additionally depended on whether the importer was included in dependency optimization.3 That divergence is exactly what lets a bug reach production only after a build, and removing it is the point of the change.

This is not hypothetical. A Storybook Next.js integration filed it under the title "NextImage renders as object with Vite 8 due to Rolldown CJS interop change", surfaced through dependency pre-bundling in stories and browser tests; the reported symptom is React refusing the component with "Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object."17 That error text is the tell — an object where a component was expected. The issue has since been closed with a linked fix, which is the normal shape of this: each affected package fixes its own unwrapping.

For a single import you can disambiguate at the call site rather than reaching for a global flag. Rolldown's documentation gives the test to use — check the __esModule marker rather than guessing:16

import raw from 'some-cjs-package'

const actual =
  typeof raw === 'object' && raw !== null && raw.__esModule ? raw.default : raw

The __esModule check is doing real work there, so resist shortening it to raw.default ?? raw. A plain CommonJS module that happens to export an object with its own default property, and does not set __esModule, is exactly the case Rolldown's heuristic hands you whole — and the shorter expression would silently unwrap it to that property instead. This paragraph is reasoning about the documented rule rather than something the docs spell out, so test it against the specific package rather than adopting it blind.

There is a deprecated global escape hatch if you need to ship today:

// vite.config.js — temporary, deprecated
export default defineConfig({
  legacy: { inconsistentCjsInterop: true },
})

The migration guide is explicit that this is a stopgap and asks you to report affected packages upstream, with a link to the Rolldown documentation so the maintainer has the context.3

Two adjacent CommonJS changes travel with it. require calls for externalized modules are now preserved as require rather than converted to import; if you needed the old behaviour, Vite re-exports Rolldown's esmExternalRequirePlugin for it — which matters mainly for SSR and Node-targeted builds.3 And import.meta.url is no longer polyfilled in UMD or IIFE output, where it is replaced with undefined by default.3 That second one deserves a moment if you publish a library: new URL('./worker.js', import.meta.url) is the documented Vite pattern for workers and asset URLs, and in a UMD build it now becomes new URL('./worker.js', undefined) — a runtime error in your consumers, from a build that reported success. The guide's stated remedy is the define option combined with build.rolldownOptions.output.intro.3

Why do my decorators fail to compile in Vite 8?

Start by checking which kind of decorators you have, because the answer is different and most people have the kind that still works.

TypeScript's experimentalDecorators — the ones Angular, NestJS, TypeORM and MobX use — are supported, and you probably do not have to configure anything. Oxc's transformer documentation is explicit: "Oxc transformer supports transforming legacy decorators. This is called experimental decorators in TypeScript. If you are using the experimentalDecorators option in the tsconfig, you can use the decorator.legacy option."18 More usefully, Vite "respects some of the options in tsconfig.json and sets the corresponding Oxc Transformer options," and experimentalDecorators is on that list — so "experimentalDecorators": true in your tsconfig is the documented route, and Vite maps it across for you.19 If you do want to set it from the Vite config, that wins over tsconfig when both are present, and the path follows from Vite's oxc option extending Oxc's options: oxc.decorator.legacy.1920

Native decorators — the TC39 stage 3 ones — are not lowered by Oxc yet, and the Vite team is waiting on the specification to progress.3 If that is what you have, the migration guide documents two workarounds: @rolldown/plugin-babel with @babel/plugin-proposal-decorators, and @rollup/plugin-swc with @swc/core. Both are filtered to files containing an @, using rolldown: { filter: { code: '@' } } and Vite's withFilter helper respectively.3 The filters matter: reintroducing a JavaScript transform across every file gives back the build time Rolldown just saved.

npm install -D @rolldown/plugin-babel @babel/plugin-proposal-decorators
// vite.config.ts
import { defineConfig } from 'vite'
import babel from '@rolldown/plugin-babel'

function decoratorPreset(options) {
  return {
    preset: () => ({
      plugins: [['@babel/plugin-proposal-decorators', options]],
    }),
    // Only run this transform if the file contains a decorator.
    rolldown: { filter: { code: '@' } },
  }
}

export default defineConfig({
  plugins: [babel({ presets: [decoratorPreset({ version: '2023-11' })] })],
})

Both of the guide's snippets target decorator version 2023-11, so they are behaviourally comparable — but if you adapt either one, keep the version pinned rather than letting the plugin default decide, because the revisions differ in field-initialisation order.

On the metadata side, Vite 8 did add built-in support for TypeScript's emitDecoratorMetadata, which previously needed an external plugin — but the docs qualify it: "This option is only partially supported. Full support requires type inference by the TypeScript compiler, which is not supported."19 Oxc's own note is that it "will fallback to Object type if it cannot calculate the type of the decorator metadata."18 For a dependency-injection container that keys on parameter types, an Object fallback is a runtime failure, not a rounding error.

One transform is genuinely unavailable: the migration guide lists "Transforming to ES5 and below with plugin-legacy is not supported."3 The tracking issue it points at is still open, and its reporter has published a working Babel patch to buildPolyfillChunk() that produces an ES5 polyfills chunk — so "unsupported" here means "no supported path", not "no path at all".21 If you ship to an ES5 matrix, that issue is where to look before concluding you cannot upgrade.

Does Vite 8 still work with Yarn PnP?

Partly. The advice most commonly repeated in 2026 write-ups — switch nodeLinker to node-modules before upgrading — rests on reports that are now closed, while a newer report remains open downstream. This is the claim on this page most worth testing against your own project, so what follows is the trail rather than a verdict.

The reports that built the reputation are all in rolldown-vite, and all closed. "rolldown-vite does not play well with Yarn PnP" was opened on June 4, 2025 and describes builds consistently failing to resolve external dependencies under PnP.22 "Native resolver + yarn pnp: [sass] Error: Can't find stylesheet to import." followed on August 25, 2025, and isolates the failure to the native-plugin resolver path.23 "vite 8.0.0-beta.0 cannot resolve react dependency in Yarn PnP" was opened on December 5, 2025 and closed with a linked Rolldown pull request.24 That repository was archived on March 19, 2026 and is read-only, so the staleness of all three is structural rather than incidental.22

Upstream, the tracking issue on Oxc's resolver is closed with a linked pull request, and its body states plainly: "Yarn PnP now works in v11.5.0 without any configuration."25 The page states no caveats. It also does not restate which project that version number belongs to — contextually oxc-resolver, but the actionable move is to check what is actually installed rather than to trust the number: npm ls rolldown will give you the Rolldown version in your tree, and Rolldown's release notes name the resolver version it bundles.

Cutting the other way, a Vitest issue opened on March 5, 2026 — "Regression in 4.1.0-beta.4 in Yarn PnP mode, tests hanging indefinitely on Vite 8" — was still open at the time of writing and labelled upstream.26 Read the mechanism before you weight it: the reporter's own diagnosis is that Vitest imports an optional dependency, @opentelemetry/api, which strict PnP refuses to resolve because it is not declared, and that installing it fixes the problem.26 That is a packaging regression surfaced by PnP's strictness, not evidence of a Rolldown resolver defect — which is exactly why it is worth naming precisely instead of citing it as proof.

So: do not rewrite your .yarnrc.yml on the strength of a 2025 issue in an archived repository, and do not assume PnP is clean either. Run the build and the test suite. nodeLinker: node-modules is a fallback you may not need.

Why does my build now target newer browsers than before?

Because the default value of build.target moved. Vite 8's 'baseline-widely-available' resolves to ['chrome111', 'edge111', 'firefox114', 'safari16.4', 'ios16.4'], pinned to Baseline Widely Available as of 2026-01-01.2 On Vite 7 the same special value resolved to ['chrome107', 'edge107', 'firefox104', 'safari16'].27

Nothing errors. Your output simply stops being transpiled for the browsers between those two lines, and if your support matrix includes them you have shipped a regression without a warning. Set build.target explicitly if you have a real matrix to hit — the value must be a valid Oxc Transformer target, and Vite will warn at build time if your code contains features Oxc cannot safely transpile.2

The second default change in the same area is build.minify, which is now 'oxc' for client builds. Vite describes the Oxc Minifier as "30 ~ 90x faster than terser and only 0.5 ~ 2% worse compression."2 If you suspect the minifier is miscompiling your code, the migration guide points to both minifiers' published assumption documents so you can diff them rather than guess.3

Do I still need esbuild installed?

Only if you opt back into it. The migration guide says esbuild "is no longer directly used by Vite and is now an optional dependency"; in the published package metadata for 8.2.2 that takes the form of peerDependenciesMeta.esbuild.optional: true, with the version range ^0.27.0 || ^0.28.0 declared under peerDependencies.34

You need to add it as a devDependency in three cases:32

  • build.minify: 'esbuild', which is itself deprecated
  • build.cssMinify: 'esbuild' — the most common reason today, per the CSS section above
  • any plugin that calls transformWithEsbuild, which is deprecated in favour of transformWithOxc

If none of those apply, leave it out. Adding esbuild back "just in case" reintroduces a dependency the upgrade was designed to drop.

How do I find out which of my plugins is breaking the build?

Bisect first, then read. The fastest route to the culprit is to halve your plugins array, rebuild, and repeat — a handful of builds beats reading a transitive plugin tree, and it works even when the failing plugin is one your framework installed rather than one you chose.

Three things sharpen that loop:

  • vite build --debug for Vite's own debug output on the failing step.
  • VITE_DEPRECATION_TRACE=1 when the failure is a deprecated option rather than a crash. It turns the warning into a stack trace, which is the most direct way to tell which plugin set an option you never wrote.6
  • Peer ranges. Check each plugin's declared peerDependencies on vite. A plugin that has not widened its range to ^8 is telling you something before you read a line of its source.

Once you have a suspect, four structural differences account for a great deal of plugin breakage:3

  1. Missing hooks. shouldTransformCachedModule, resolveImportMeta, renderDynamicImport, and resolveFileUrl are listed under "Missing support by Rolldown" and are no longer supported. The guide does not say what an affected plugin does at runtime, so treat a plugin that uses one as suspect rather than waiting for a diagnostic message.
  2. Sequential hooks. "All parallel hooks in Rollup work as sequential hooks" under Rolldown.
  3. The bundle object. Assigning to bundle[foo] is no longer supported — use this.emitFile(). The reference is not shared across hooks, and structuredClone(bundle) now throws DataCloneError; clone { ...bundle } instead.
  4. Module types. Rolldown auto-assigns a module type from the resolved id's extension. A plugin converting non-JS content to JavaScript in load or transform may need to return moduleType: 'js' explicitly.

Vite launched registry.vite.dev alongside Vite 8, a searchable directory of Vite, Rolldown and Rollup plugins built from daily npm data, which is the right place to check a plugin's current state rather than any static list.1

If you call Vite's JavaScript API directly, one more change matters: build() now throws a BundleError, typed as Error & { errors?: RolldownError[] }, which wraps individual errors in an array. Existing catch blocks that read e.message will report something far less useful than e.errors.3

Why did a patch release of Vite 8 break a build that worked yesterday?

Because Rolldown is still moving underneath Vite's patch releases, and two documented regressions landed that way. This is its own bucket because the diagnostic is different: nothing in your config changed, so the useful first step is to pin backwards and confirm, not to edit anything.

An issue opened April 24, 2026 reports Astro builds failing after updating from 8.0.8 to 8.0.10 with Missing field 'tsconfigPaths' on BindingViteResolvePluginConfig.resolveOptions, thrown from @tailwindcss/vite; the reporter attributes it to a Rolldown release-candidate bump, and a second variant of the same symptom names a different missing field.28 It was closed as not planned.

An issue opened July 1, 2026 reports a build failing from 8.1.0 onward with [MISSING_EXPORT] "placements" is not exported by "node_modules/@popperjs/core/lib/index.js", on a project that builds cleanly on 8.0.16.29 It was closed as a duplicate. Two details from it are worth carrying: the reporter's stated workaround is literally npm install vite@=8.0.16, and the failure is build-only — "There is no issue with npm run dev", which is the trap, because a green dev server is not evidence.29

The practical response is to treat the Vite patch version as a variable in your bisect. If the build broke without a config change, install the previous patch, confirm, and then read the changelog between the two rather than searching your own source.

Why does it build locally but fail in CI?

Because two Vite 8 failure modes are properties of the environment rather than of your code, and neither is about your Node version.

The native binary. Rolldown ships its platform-specific binaries as npm optionalDependencies — fifteen of them in the current release, one per target.30 That is the same packaging pattern that produced Rollup's long-running install failures, and Rolldown inherits it. The outer symptom is Error: Cannot find native binding, wrapping a Cannot find module '../rolldown-binding.<platform>.node'.31 Rolldown's troubleshooting page addresses that inner error under the heading Error: "Cannot find module '@rolldown/binding-...'" and names the cause: "It is usually caused by a known npm bug with optional dependencies (npm/cli#4828); if you installed with npm, removing node_modules and package-lock.json and reinstalling fixes it."32 The same page documents a second cause worth knowing if you develop across Windows and WSL: a config file in a symlinked directory can resolve rolldown to a node_modules installed for a different platform, for which the remedies are keeping the config outside the symlinked directory or setting NODE_OPTIONS=--preserve-symlinks — with the caveat, stated on that page, that the flag is "not compatible with pnpm, whose node_modules layout relies on symlinks."32

A community report adds one more trigger that is easy to miss because it looks unrelated. A commenter found that changing their package.json engines field from "node": ">22" to ">=22.18.0" "caused the binary to download", and flagged the package manager's minimumReleaseAge setting as another possible culprit.31 The semver reading is mine rather than theirs, and it is at least consistent: >22 resolves to >=23.0.0, which excludes the 22.12–22.x range Rolldown declares support for.30 If your CI image is Alpine or musl-based, also check that you are getting the musl binding rather than the gnu one.

Memory. This is the one the install-size figures do not prepare you for. An open, assigned Rolldown issue on the "Vite 8 migration blockers" board reports roughly a sevenfold increase in physical memory footprint in dev mode — about 880 MB on Vite 7.3.3 against about 6.7 GB on Vite 8.0.11 — in one large project.33 Read the reporter's own caveats with it: they have not separately bisected @vitejs/plugin-react 4 to 5, so part of the regression may live there; there is no minimal reproduction yet; and the regression is present from the first Vite 8 beta rather than appearing in a specific patch, with the drift across stable releases "dwarfed by the v7→v8 step."33 It is one project's measurement, not a benchmark. But on a constrained CI runner an out-of-memory kill often surfaces as a non-zero exit with no useful message, which reads exactly like "the build fails after upgrading."

Neither of these is covered by Vite's or Rolldown's documentation beyond the native-binding page, so treat memory in particular as something to measure on your own runner rather than something with a documented answer.

How do I migrate gradually, or roll back?

Use the official two-step path forward. The rolldown-vite package implements Vite 7 with Rolldown and none of the other Vite 8 changes, which isolates bundler problems from everything else, and Vite's recommendation for larger projects is to switch to it on Vite 7 first, then upgrade.31 Leaving it behind is the one-line change the guide shows — replacing a "vite": "npm:rolldown-vite@7.2.2" entry with "vite": "^8.0.0".3

Rolling back to Vite 7 is more than a version bump, because the upgrade pulled coupled majors with it. Clear the lockfile and tree first, so the reinstall resolves cleanly rather than against stale entries for rolldown, lightningcss and the native bindings:

rm -rf node_modules package-lock.json
# edit package.json: vite ^7, and revert your framework plugin's major
npm install

Two things to check while you are there. Your framework plugin needs a version whose peer range accepts Vite 7 — @vitejs/plugin-react@5.2.0 declares ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0, which makes that specific release a safe stopping point in both directions; earlier 5.x releases stop at Vite 7, and the v6 line requires Vite 8.34 And note that node_modules/.vite is Vite's dependency-optimization cache and lives inside node_modules, so deleting the latter already clears it — but a cache written by Rolldown is not something Vite 7 should read, so do not skip that step. The Vite 7 documentation stays live at v7.vite.dev, which the Vite 8 docs link directly.3

Vitest is not the constraint people assume it is: vitest@4.1.11 declares its Vite peer range as ^6.0.0 || ^7.0.0 || ^8.0.0, so that line spans three Vite majors and a rollback does not force a Vitest downgrade.35 Check your own installed version rather than assuming, since the range has moved across Vitest releases.

One caution on sequencing that does apply to some stacks. On a plain React or Vue app you can upgrade Vite first and the plugin afterwards, which makes a bisect meaningful.1 On a meta-framework — Storybook, Nuxt, SvelteKit, Astro, React Router — the peer range often decides the order for you, and your package manager will either refuse the install or produce a broken tree. Check the framework's supported Vite range before you plan the sequence rather than after. If you do move @vitejs/plugin-react to v6, it uses Oxc for the React Refresh transform and drops Babel as a dependency, with a reactCompilerPreset helper for projects that need the React Compiler — a different setup from the Babel-based one v5 shipped, and worth reading about first if you have ever debugged React Compiler not memoizing a component.1

Is the build actually faster, and what does it cost?

Faster, on the available evidence, and the evidence is entirely vendor-supplied. Vite reports Rolldown as 10-30x faster than Rollup in benchmarks, and names four companies with measured production build times: Linear from 46s to 6s, Ramp with a 57% reduction, Mercedes-Benz.io up to 38%, and Beehiiv 64%.1 Those were reported by the adopters during the preview and beta phases, on their own codebases. I found no independent published benchmark of Vite 8 against Vite 7 as of September 2026, so treat the range as directional.

The disclosed cost is about 15 MB of additional install size versus Vite 7: roughly 10 MB because Lightning CSS moved from an optional peer dependency to a normal one, and roughly 5 MB for the Rolldown binary, which is larger than esbuild plus Rollup because it favours speed over binary size.1 Vite says it will keep working to reduce this.1 The disk figure is the disclosed cost; the memory figures in the CI section above are the undisclosed one.

Node requirements did not change: Vite 8 needs Node 20.19+ or 22.12+, the same as Vite 7, and the published engines field for 8.2.2 confirms ^20.19.0 || >=22.12.0.14 That makes the Node version a non-issue — but as the CI section covers, it is not the only thing about your CI environment that matters.

Bottom line

Vite 8's compatibility layer does more work than its reputation suggests, and a lot of the migration advice circulating in 2026 spends its opening paragraphs on a rename that is not required to get a build green. The failures that actually stop builds are narrower and more specific: a stricter CSS minifier, an ESM-only rule for top-level await, unified CommonJS semantics, native decorators with no lowering step, a short list of removed options — and, for a good number of teams, an environment problem in CI that has nothing to do with any of them.

Four steps, in order. Run the build unchanged and read the error against the seven buckets above. If it fails only in CI, go to the native-binary and memory section before you touch config. If the failure is in CSS, try a Lightning CSS override or build.cssTarget before reverting the whole pipeline. And if it is anything else, register a configResolved plugin and print what the compatibility layer produced before you change a single line.

Then leave the deprecation cleanup for a separate commit — and when you do it, move each rollupOptions object across whole, since a half-migrated config silently loses the half you left behind. If you are also modernising the rest of your toolchain, the same "Rust rewrite, same API surface" pattern is worth reading about in TypeScript 7's native compiler, and if this upgrade is bundled with a CSS overhaul, Tailwind v4 with Vite and CSS-first @theme config covers the plugin side of that pairing.

References

Footnotes

  1. "Vite 8.0 is out!", Vite blog, March 12, 2026 — https://vite.dev/blog/announcing-vite8 — release date, Rolldown rationale, 10-30x claim, named adopter build times, install size breakdown, Node requirements, @vitejs/plugin-react v6, registry.vite.dev, gradual migration recommendation. 2 3 4 5 6 7 8 9 10 11 12

  2. "Build Options", Vite configuration reference — https://vite.dev/config/build-optionsbuild.rollupOptions documented as a deprecated alias, build.target resolved values, build.cssMinify and build.minify defaults, build.cssTarget precedence, esbuild install requirement. 2 3 4 5 6 7 8 9 10 11 12 13 14

  3. "Migration from v7", Vite documentation — https://vite.dev/guide/migration — deprecated vs removed vs unsupported options, auto-conversion tables, CJS interop rules, decorator workarounds, browser target change, plugin and JS API changes. Retrieved September 3, 2026 against the Vite 8.2.2 docs. 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

  4. vite@8.2.2 package metadata, npm registry — https://registry.npmjs.org/vite/latest — current version, engines field, rolldown and lightningcss dependencies, and esbuild listed under peerDependencies (^0.27.0 || ^0.28.0) with peerDependenciesMeta.esbuild.optional: true. 2 3 4 5

  5. "Worker Options", Vite configuration reference — https://vite.dev/config/worker-optionsworker.format type 'es' | 'iife' with default 'iife'; worker.rollupOptions documented as a deprecated alias. 2 3

  6. Vite source, packages/vite/src/node/utils.tshttps://github.com/vitejs/vite/blob/main/packages/vite/src/node/utils.ts — the comment and implementation "if both rollupOptions and rolldownOptions are present, ignore rollupOptions and use rolldownOptions"; the deprecation warning gated to optimizeDeps and ssr.optimizeDeps; and the VITE_DEPRECATION_TRACE switch from console.warn to console.trace. The plugin-level "will be ignored" warning is in packages/vite/src/node/config.ts. 2 3 4 5 6 7 8

  7. "OutputOptions.codeSplitting" and "Manual Code Splitting", Rolldown documentation — https://rolldown.rs/reference/OutputOptions.codeSplitting and https://rolldown.rs/in-depth/manual-code-splitting — the groups / name / test shape, the forced runtime.js chunk, and recursive dependency capture. 2 3

  8. "Default CSS minifier in Vite@8 blocks some progressive modern CSS features", vitejs/vite issue #21911, opened March 17, 2026 — https://github.com/vitejs/vite/issues/21911@scope rejected during minification; open and labelled bug: upstream at the time of writing. 2 3

  9. "Vite 8 default cssMinify: 'lightningcss' drops unprefixed backdrop-filter, breaking glass/blur effects (regression vs Vite 7)", vitejs/vite issue #22649, opened June 9, 2026 — https://github.com/vitejs/vite/issues/22649 — reported on Vite 8.0.16 with lightningcss 1.32.0; closed as a duplicate of #21954. 2

  10. "backdrop-filter before -webkit-backdrop-filter is dropped with cssMinify: 'lightningcss'", vitejs/vite issue #21954, opened November 20, 2025 — https://github.com/vitejs/vite/issues/21954 — open and labelled bug: upstream at the time of writing; the issue #22649 was closed against. 2 3

  11. "TLA in Rolldown", Rolldown documentation — https://rolldown.rs/in-depth/tla-in-rolldown — "If your input contains TLA, it could only be bundled and emitted with esm format." 2 3

  12. "Build fails with top-level await in @novnc/novnc after 8.0.6+ rolldown switch", vitejs/vite issue #22314, opened April 23, 2026 — https://github.com/vitejs/vite/issues/22314 — 8.0.10 fails where 8.0.5 succeeded; closed, with no workaround stated on the page. 2

  13. "[Vite 8] build() library mode: resolve.alias does not work unless included in rolldownOptions", vitejs/vite discussion #22377, opened May 1, 2026 — https://github.com/vitejs/vite/discussions/22377 — reported on Vite 8.0.10, with the error Rolldown failed to resolve import "@/model/Alert.model". The marked answer is the author's own note that he opened issue #22410. 2 3

  14. "[Vite 8] build() library mode: resolve.alias does not work unless included in build.rolldownOptions", vitejs/vite issue #22410, opened May 8, 2026 — https://github.com/vitejs/vite/issues/22410 — reported on Vite 8.0.11; closed as not planned. Contains the reproduction showing the standalone build succeeding and only the Cypress preprocessor path failing, and the error Rolldown failed to resolve import "@/foo.js". 2 3 4 5

  15. "Troubleshooting — Default import unexpectedly returns an object", Vite documentation — https://vite.dev/guide/troubleshooting — "The default import returns the module.exports object for CJS modules, while you may expect it to return the module.exports.default value." 2

  16. "Bundling CJS", Rolldown documentation — https://rolldown.rs/in-depth/bundling-cjs — the full condition list for ambiguous default imports from CJS modules, which is longer than the three conditions Vite's migration guide states, and the __esModule test used to disambiguate a single import. 2 3

  17. "[Bug] NextImage renders as object with Vite 8 due to Rolldown CJS interop change", storybookjs/vite-plugin-storybook-nextjs issue #114, opened March 24, 2026 — https://github.com/storybookjs/vite-plugin-storybook-nextjs/issues/114 — a real-world instance of the symptom, reported via dependency pre-bundling in stories and browser tests, with React's "Element type is invalid … but got: object" as the surfaced error. Closed, with a linked pull request.

  18. "TypeScript", Oxc Transformer documentation — https://oxc.rs/docs/guide/usage/transformer/typescript — "Oxc transformer supports transforming legacy decorators. This is called experimental decorators in TypeScript", the decorator.legacy and decorator.emitDecoratorMetadata options, and the Object type fallback for decorator metadata. 2 3

  19. "Features", Vite documentation — https://vite.dev/guide/featuresemitDecoratorMetadata support is "only partially supported. Full support requires type inference by the TypeScript compiler, which is not supported." 2 3 4

  20. "Shared Options", Vite configuration reference — https://vite.dev/config/shared-options — the oxc option, documented as extending Oxc Transformer's options.

  21. "legacy: support lowering to ES5", vitejs/vite issue #21951, opened October 17, 2025 — https://github.com/vitejs/vite/issues/21951 — the tracking issue the migration guide points at for @vitejs/plugin-legacy; open, and containing a reporter-published Babel patch to buildPolyfillChunk().

  22. "rolldown-vite does not play well with Yarn PnP", vitejs/rolldown-vite issue #215, opened June 4, 2025 — https://github.com/vitejs/rolldown-vite/issues/215 — closed; the repository banner records that it was archived on March 19, 2026 and is read-only. 2 3

  23. "Native resolver + yarn pnp: [sass] Error: Can't find stylesheet to import.", vitejs/rolldown-vite issue #392, opened August 25, 2025 — https://github.com/vitejs/rolldown-vite/issues/392 — closed, with a linked rolldown/rolldown pull request; reproduces only with native plugins enabled. 2

  24. "vite 8.0.0-beta.0 cannot resolve react dependency in Yarn PnP", vitejs/rolldown-vite issue #543, opened December 5, 2025 — https://github.com/vitejs/rolldown-vite/issues/543 — closed, with a linked rolldown/rolldown pull request. 2

  25. "Yarn PnP", oxc-project/oxc-resolver issue #53, opened January 12, 2024 — https://github.com/oxc-project/oxc-resolver/issues/53 — closed with a linked pull request; body states "Yarn PnP now works in v11.5.0 without any configuration." 2

  26. "Regression in 4.1.0-beta.4 in Yarn PnP mode, tests hanging indefinitely on Vite 8", vitest-dev/vitest issue #9799, opened March 5, 2026 — https://github.com/vitest-dev/vitest/issues/9799 — open and labelled upstream at the time of writing; the reporter identifies an undeclared optional dependency, @opentelemetry/api, as the thing strict PnP refuses to resolve, and notes that installing it fixes the problem. 2 3

  27. "Build Options", Vite 7 configuration reference — https://v7.vite.dev/config/build-options — Vite 7's 'baseline-widely-available' resolves to ['chrome107', 'edge107', 'firefox104', 'safari16']. 2

  28. "Vite 8.0.10 public resolver APIs produce incomplete rolldown plugin configs", vitejs/vite issue #22322, opened April 24, 2026 — https://github.com/vitejs/vite/issues/22322Missing field \tsconfigPaths` on BindingViteResolvePluginConfig.resolveOptionson Astro builds via@tailwindcss/vite`; 8.0.8 working, 8.0.10 broken; closed as not planned. 2

  29. "vite resolver broken since 8.1.0", vitejs/vite issue #22835, opened July 1, 2026 — https://github.com/vitejs/vite/issues/22835[MISSING_EXPORT] "placements" is not exported by "node_modules/@popperjs/core/lib/index.js" on 8.1.2, working on 8.0.16; the reporter notes "There is no issue with npm run dev". Closed as a duplicate of #22779. 2 3

  30. rolldown package metadata, npm registry — https://registry.npmjs.org/rolldown/latest — fifteen platform-specific bindings declared under optionalDependencies, and the engines range ^20.19.0 || >=22.12.0. 2 3

  31. "[Bug]: rolldown-binding.linux-x64-gnu.node missing (pnpm install)", rolldown/rolldown discussion #9098, opened April 10, 2026 — https://github.com/rolldown/rolldown/discussions/9098Error: Cannot find native binding with librt.so.1: cannot open shared object file; unanswered at the time of writing. The engines: ">22" trigger is a commenter's finding, not a maintainer statement. 2

  32. "Troubleshooting", Rolldown documentation — https://rolldown.rs/guide/troubleshooting — the Cannot find module '@rolldown/binding-...' section, the npm optional-dependency bug (npm/cli#4828), and the symlinked-config / --preserve-symlinks case with its pnpm caveat. 2 3

  33. "[Bug]: rolldown 1.0.0-rc.18 in vite 8 dev mode shows ~7× higher physical memory footprint vs vite 7 (rollup + esbuild)", rolldown/rolldown issue #9330, opened May 9, 2026 — https://github.com/rolldown/rolldown/issues/9330 — open, assigned, labelled scope: perf and tracked on the "Vite 8 migration blockers" board; ~880 MB on Vite 7.3.3 against ~6.7 GB on Vite 8.0.11 in one project. The reporter states they have not bisected @vitejs/plugin-react 4→5 separately and have no minimal reproduction. 2 3

  34. @vitejs/plugin-react@5.2.0 package metadata, npm registry — https://registry.npmjs.org/@vitejs/plugin-react/5.2.0vite peer range ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0. Pinned deliberately: earlier 5.x releases omit Vite 8, and the v6 line requires it.

  35. vitest@4.1.11 package metadata, npm registry — https://registry.npmjs.org/vitest/4.1.11 — the vite peer dependency range ^6.0.0 || ^7.0.0 || ^8.0.0. Pinned deliberately: the /latest endpoint moves, and later Vitest releases narrow this range.

Frequently Asked Questions

No. It is documented as an alias of build.rolldownOptions and marked deprecated. It still works, and renaming it will not fix a failing build. 2