3.1 · difficulty 2/4 · 16 min read
Vite Plugins vs Rollup Plugins
How Vite, Rollup, and Rolldown plugin hooks overlap, where they differ, and how to decide which plugin shape to author.
The Short Version
A Vite plugin is Rollup/Rolldown-compatible hooks plus Vite-only hooks plus dev server semantics.
That means:
resolveId,load,transform, andgenerateBundlecome from the Rollup plugin model.- Vite 8 builds with Rolldown, while keeping Rollup-style plugin compatibility.
config,configureServer,transformIndexHtml, andhandleHotUpdateare Vite-specific.- Dev mode does not perform a full bundle, so build-only hooks do not behave like production.
Most pure transform Rollup plugins can be used in Vite. A Vite plugin that depends on the dev server, HTML handling, or HMR cannot be dropped into Rollup unchanged.
The Three-Layer Model
Vite plugin
├─ Vite-only hooks: config / configureServer / transformIndexHtml / handleHotUpdate
├─ Universal build hooks: resolveId / load / transform / renderChunk / generateBundle
└─ Rolldown/Rollup compatibility layer| Shape | Target | Typical capabilities | Works directly in Vite |
|---|---|---|---|
| Rollup plugin | Rollup/Rolldown build pipeline | resolution, loading, transforms, output hooks | usually |
| Rolldown plugin | Rolldown build pipeline | build-specific Rolldown extension points | for build |
| Vite plugin | Vite dev + build lifecycle | config, dev server, HTML, HMR, build | yes |
Vite plugins are not a replacement for Rollup plugins. They place the same build hooks inside a broader dev server model.
Portable Hooks
Hooks that only care about resolving, loading, transforming, and outputting modules are easier to reuse:
| Hook | Purpose | Common use |
|---|---|---|
options | adjust build options | defaults |
buildStart | initialize work | caches, file scans |
resolveId | resolve import IDs | aliases, virtual modules |
load | provide module source | virtual modules, custom files |
transform | transform source | DSL compilation, code injection |
renderChunk | post-process chunks | wrappers |
generateBundle | inspect final bundle | reports, emitted files |
writeBundle | after writing output | sourcemap upload |
If your plugin uses only these hooks and does not touch Vite's server, config, or module graph, author it as a Rollup/Rolldown-compatible plugin.
import type { Plugin } from "vite"
export function virtualVersionPlugin(version: string): Plugin {
const virtualId = "virtual:app-version"
const resolvedId = "\0" + virtualId
return {
name: "virtual-version",
resolveId(id) {
if (id === virtualId) return resolvedId
},
load(id) {
if (id === resolvedId) {
return `export const version = ${JSON.stringify(version)}`
}
},
}
}This does not rely on Vite-only hooks, so it is cheap to reuse elsewhere.
Vite-Only Hooks
| Hook | Why it is Vite-specific |
|---|---|
config | Vite owns config merging, mode, and env loading |
configResolved | reads Vite's final ResolvedConfig |
configureServer | Vite owns the dev server and middleware stack |
configurePreviewServer | vite preview is a Vite server |
transformIndexHtml | Vite treats HTML as an entry |
handleHotUpdate | HMR, module graph, and websocket are Vite dev concepts |
Example:
import type { Plugin } from "vite"
export function devHealthPlugin(): Plugin {
return {
name: "dev-health",
apply: "serve",
configureServer(server) {
server.middlewares.use("/__health", (_req, res) => {
res.setHeader("Content-Type", "application/json")
res.end(JSON.stringify({ ok: true }))
})
},
}
}Rollup cannot run this plugin because it has no dev server middleware.
Dev vs Build Hook Behavior
Vite dev is request-driven. Build scans and bundles the full graph.
| Question | Dev | Build |
|---|---|---|
| When is a module processed? | when requested by the browser | while traversing the full graph |
| Is a bundle generated? | no full output bundle | yes |
generateBundle | not useful | core output hook |
configureServer | called | not called |
transform | per requested module | per bundled module |
| HMR | yes | no |
Use apply when the plugin only makes sense in one phase:
export function reportBundleSize(): Plugin {
return {
name: "report-bundle-size",
apply: "build",
generateBundle(_, bundle) {
console.log(Object.keys(bundle).length)
},
}
}Ordering
Vite plugin order is grouped:
user pre plugins
Vite core plugins
user normal plugins
Vite build plugins
user post pluginsenforce: "pre" and "post" control where a plugin sits in Vite's queue. This is separate from hook-level order.
If an MDX plugin must run before React, use enforce: "pre". If an analyzer needs final output, use apply: "build" and generateBundle.
Decision Flow
Need dev server / middleware / websocket?
├─ yes -> Vite plugin
└─ no
Need to transform index.html?
├─ yes -> Vite plugin
└─ no
Need custom HMR boundaries?
├─ yes -> Vite plugin
└─ no
Only resolve/load/transform/output hooks?
├─ yes -> Rollup/Rolldown-compatible plugin
└─ no -> clarify the runtime first| Requirement | Recommended shape |
|---|---|
Convert .md to JS modules | Rollup/Rolldown-compatible plugin |
Add /api/mock to dev server | Vite plugin |
| Inject script into HTML | Vite plugin |
| Analyze build output size | build-only Vite plugin or Rolldown plugin |
Implement HMR for .yaml | Vite plugin |
| Publish to multiple bundlers | depend on universal hooks only |
Common Misconceptions
A Rollup plugin fully runs during Vite dev
Not always. Dev does not produce a full output bundle, so output hooks like generateBundle are not useful there.
A Vite plugin is automatically Rollup-compatible
If it uses a Vite-only hook, it is not Rollup-compatible.
All logic belongs in transform
It does not. Put path resolution in resolveId, virtual source in load, and output analysis in generateBundle. Overusing transform makes plugins slow and hard to debug.
Check Yourself
- Why is
transformIndexHtmlVite-only? - Why are
resolveId/load/transformeasier to reuse across tools? - Why is
generateBundlenot a good dev-mode debugging entry point? - What plugin shape should a dev mock API use?
// Pick the plugin shape and explain why:
// 1. Convert .txt files into JS string modules
// 2. Add /__debug that returns the current module graph
// 3. Emit bundle-size.json after build
// 4. Inject an analytics script into index.html