Vite 与 Webpack 深度对比

Vite 和 Webpack 的架构差异:为什么 Vite 在开发体验上远超 Webpack、两者在生产构建上的实际差距、esbuild 和 Rolldown 在构建链路中的角色,以及如何根据项目特点做技术选型。

Vite 与 Webpack 深度对比

问题的起点:开发体验的痛苦

用 Webpack 做过中大型项目的人,大概都经历过这种场景:改一行 CSS,等 10 秒看效果;重启 dev server,要等半分钟。这不是配置问题,是 Webpack 架构上的根本限制。

Webpack 的开发 server 本质上是把整个项目打包成一个巨大的 bundle,然后启动一个本地服务器。任何一次代码变更,都需要重新走完整个打包流程。即使只改了一个组件里的几行代码,Webpack 也要从入口开始重新解析、编译、链接所有模块。

Vite 正是针对这个痛点设计的。它在开发阶段完全不打包,只在浏览器请求时提供 ES Module 格式的原始文件。这带来了截然不同的开发体验:启动即时、修改即时(通常 < 100ms)、热更新只涉及修改的文件而非整个 bundle。

但 Vite 不是 Webpack 的简单替代,两者各有优劣。要理解它们,先要理解各自的架构。


Webpack 的架构

核心:依赖图与模块打包

Webpack 的核心是依赖图。从入口文件开始,Webpack 递归解析所有 importrequire 语句,构建出完整的模块依赖图。然后根据配置,把这张图打包成一个(或多个)最终 bundle。

flowchart TD
    Entry["入口文件<br/>src/index.js"] --> Resolver["依赖解析<br/>resolve"]
    Resolver --> Module1["模块 1<br/>import x from './a'"]
    Resolver --> Module2["模块 2<br/>import y from './b'"]
    Resolver --> Module3["模块 3<br/>import z from './c'"]
    Module1 --> Loader1["loader 处理<br/>ts-loader/babel"]
    Module2 --> Loader2["loader 处理"]
    Module3 --> Loader3["loader 处理"]
    Loader1 --> Bundler["打包<br/>webpack bundler"]
    Loader2 --> Bundler
    Loader3 --> Bundler
    Bundler --> Output["输出<br/>dist/bundle.js"]

这个架构的特点是先分析后执行:dev server 启动时,Webpack 要完整扫描和编译整个项目,然后才能提供服务。

Loaders 与链式转换

Webpack 本身只理解 JavaScript。处理其他类型的文件(TypeScript、CSS、图片、Vue 单文件组件)需要 loader。每个 loader 负责把一种资源转换成 Webpack 能理解的模块:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,       // 匹配 .ts/.tsx 文件
        use: 'ts-loader',      // 用 ts-loader 处理
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

链式 loader 是从右到左执行的:css-loader 先读取 CSS 文件,style-loader 再把样式注入到 DOM。

生产构建的代码优化

Webpack 在生产构建时会做大量代码优化:Tree Shaking(移除未使用的导出)、代码分割(code splitting)、压缩(minification)、scope hoisting(合并模块减少函数包装)。这些优化依赖静态分析——Webpack 通过分析 ES Module 的 import/export 判断哪些代码是死代码。

这也带来一个现实问题:Webpack 的 Tree Shaking 能力取决于你写的代码风格。用 CommonJS require() 或者大量动态导入,Tree Shaking 效果会大打折扣。


Vite 的架构

开发阶段:原生 ESM + 按需编译

Vite 在开发阶段不打包。它的 dev server 启动时只做两件事:启动一个本地 HTTP 服务器,配置好对各类文件的处理规则。当浏览器请求一个文件时,Vite 按需编译那个文件,然后返回。

flowchart LR
    Browser["浏览器<br/>请求 app.js"] --> Server["Vite Dev Server<br/>localhost:5173"]
    Server --> Transform["按需编译<br/>esbuild"]
    Transform --> ESM["ES Module<br/>返回给浏览器"]
    ESM --> Browser
    Browser --> Import["浏览器解析 import<br/>再次请求模块"]
    Import --> Transform

浏览器发送的 import 请求是针对每个文件的,Vite 可以并行处理这些请求。第一次打开页面时可能触发几十个文件的编译,但每个文件的编译是独立的,不会像 Webpack 那样串行处理整个依赖树。

这就是为什么 Vite 的启动时间是 O(1) 或 O(n)(文件数量),而 Webpack 的启动时间是 O(n²) 或更差——Webpack 要处理的不只是文件,还有文件之间的链接和依赖图。

esbuild:极速编译

Vite 使用 esbuild 做 TypeScript 和 JSX 的转译。esbuild 是用 Go 编写的,比 Babel 快 10-100 倍。

// vite.config.js
export default {
  esbuild: {
    jsxFactory: 'React.createElement',
    jsxFragment: 'React.Fragment',
  },
};

esbuild 在 Vite 里做的是转译(把 TS/JSX 转成标准 JS),而不是打包。转译后的文件交给浏览器,浏览器的原生 ES Module 支持来处理模块间的链接。

生产构建:Rolldown

Vite 的生产构建默认使用 Rolldown(Webpack 的 rollup.js 的 Rust 重写版)。Rolldown 比 Webpack 快很多,但功能子集略有不同,还在持续完善中。

// vite.config.js
export default {
  build: {
    rollupOptions: {
      // Rolldown 配置
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
        },
      },
    },
  },
};

Rolldown 兼容大部分 Rollup 插件生态,同时也借鉴了 Webpack 的一些概念。Vite 的插件系统也同时支持 Rollup 插件和 Vite 特有的插件。


核心差异对比

启动速度

这是两者差距最明显的地方:

项目规模 Webpack dev 启动 Vite dev 启动
小(< 100 模块) 2-5 秒 < 1 秒
中(100-1000 模块) 10-30 秒 1-3 秒
大(> 1000 模块) 30 秒 - 数分钟 3-10 秒

Vite 的启动速度几乎不受项目规模影响,因为不需要完整打包。Webpack 的启动时间随模块数量线性或超线性增长。

热更新速度

Webpack 的热更新需要:重新编译变更模块 → 重新生成 chunk → 通过 WebSocket 通知浏览器更新。每次修改都可能影响模块图结构,Webpack 需要重新计算依赖。

Vite 的热更新是文件级别的:修改的文件通过 HTTP 返回新内容(esbuild 重新编译该文件),浏览器通过原生 ESM 更新模块图。模块图是浏览器自己维护的,不需要 Vite 重新计算。

对于 CSS 修改,Vite 直接替换 CSS 样式,不会触发 JS 重跑。对于 JS 修改,Vite 只更新涉及变化的模块。

生产构建质量

这是 Webpack 目前还有优势的地方。

Webpack 的生产构建有 decades 积累的优化策略:Terser 压缩、scope hoisting、 ModuleConcatenationPlugin、Tree Shaking 的精确分析、长期缓存优化(contenthash)。Webpack 的 splitChunks 策略在处理复杂的多 entry 共享依赖场景时更成熟。

Rolldown(Vite 生产构建)虽然在速度上大幅领先,但部分高级特性(如精确的 Tree Shaking、对某些模块格式的处理)仍在追赶中。对于绝大多数项目,Rolldown 的输出质量和 Webpack 差距不大;对于超大复杂项目,Webpack 的优化策略可能产生更小的 bundle。

生态与插件

Webpack 的插件生态极为丰富,这是它多年积累的优势。任何你能想到的功能——Svelte 组件支持、CSS Modules、SSR 构建、微前端——都有成熟插件。

Vite 的插件生态在快速增长,大部分 Rollup 插件可以直接使用,Vite 特有的插件也越来越丰富。但某些小众场景下,Vite 可能找不到成熟的解决方案。


什么时候选 Webpack

选 Webpack 的场景:

  • 已经在生产环境使用 Webpack 的巨型项目,迁移成本高
  • 需要极度精细的 bundle 优化控制(比如对第三方库做细致的代码分割策略)
  • 使用了某些只有 Webpack 支持的特殊插件或加载器
  • 项目对 Tree Shaking 和 bundle 大小有极致优化需求,且现有构建链路已经调优到位

不选 Webpack 的场景:

  • 新项目或者可以迁移的中型项目
  • 开发体验已经成为团队痛点
  • 项目主要用 Vue(Vite 对 Vue SFC 支持非常好)
  • 需要快速启动、快速热更新

迁移路径:从 Webpack 到 Vite

如果决定从 Webpack 迁移到 Vite,步骤通常是:

第一步:安装 Vite 和插件。

npm install vite @vitejs/plugin-react  # React 项目
npm install vite @vitejs/plugin-vue    # Vue 项目

第二步:创建 vite.config.js。

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

export default defineConfig({
  plugins: [react()],
  // 大部分 Webpack 配置在 Vite 里是内置或不需要的
});

第三步:处理兼容性问题。

  • require.context(Webpack 特有)在 Vite 里需要用 import.meta.glob 替代
  • Webpack loader 链需要改写成 Vite 插件或 Rollup 插件
  • process.env 在 Vite 里用 import.meta.env 替代
  • CSS Modules、less/sass 等需要配置对应的 Vite 插件

第四步:调整 npm scripts 和环境变量。

Vite 的 dev server 默认端口是 5173,构建产物默认输出到 dist。环境变量文件是 .env 而不是 Webpack 的 .env

迁移的最大工作量通常在第三步——那些依赖 Webpack 特有 API 的代码需要重写。但因为 Vite 对标准 ES Module 的原生支持,很多之前需要 loader 处理的事情直接就工作了。


面试中的表达

展示系统理解的 Webpack vs Vite 回答:

Webpack 和 Vite 的本质差异在于开发阶段的处理方式。Webpack 用的是「先打包后服务」模式——启动时完整扫描依赖图、编译所有模块,构建出一个 bundle 才提供服务。这让它的启动时间和热更新速度随项目规模线性增长。Vite 用的是「原生 ESM + 按需编译」模式——dev server 启动只启动 HTTP 服务器,不打包;浏览器请求哪个文件,Vite 才编译哪个文件,浏览器自己维护模块图。这让 Vite 的启动时间是 O(1),热更新是文件级别。但生产构建端,Webpack 的优化策略 decades 积累更成熟,Rolldown 虽然在追赶,对大多数项目差距已经不大。选择哪个要看项目的阶段——新项目建议 Vite,巨型复杂项目且构建质量要求极高,Webpack 目前还有优势。


延展阅读