Vite 构建缓存与持久化缓存策略:依赖预构建、Rollup 缓存与 CI 加速

Vite 构建缓存全解:缓存的三层结构(依赖预构建/.vite、esbuild、Rollup)、本地开发缓存 vs 生产构建缓存的差异、缓存失效与命中判定(package.json、lockfile、配置)、CI 里的持久化缓存配置、增量构建与 Turbopack 对比,以及缓存安全与坑。

引言

Vite 的"快"很大程度建立在缓存上:依赖预构建缓存、esbuild 缓存、Rollup 的模块图缓存——但缓存用错就会命中了不该命中的、失效了不该失效的。本文把 Vite 的缓存体系拆开:先讲三层缓存的职责与存放位置(node_modules/.vite、node_modules/.cache、内存),再讲命中与失效判定(为什么改了 package.json 缓存就没了、为什么没改却没命中),接着讲生产构建缓存的现实(vite build 每次重跑 vs 增量工具)、CI 里的持久化缓存配置(关键!见 /vite-ci-cd-optimization/)、常见坑(.vite 被误清、缓存目录进 Git),最后给一套缓存治理清单。

前置:/vite-dependency-pre-bundling/(依赖预构建的机制)、/vite-ci-cd-optimization/(CI 缓存实战)、/vite-hmr-internals/(模块图与内存缓存)。


目录


1. Vite 缓存的三层结构

Vite 的缓存分三个层级,各管一段:

层位置内容生命周期
依赖预构建node_modules/.vite依赖的 esbuild 优化产物随 package.json 变化失效
转换缓存node_modules/.cacheesbuild 转译结果源码变化失效
内存模块图内存dev server 的模块图进程重启清空
第一层(.vite/deps):npm 依赖被预构建成优化后的 ESM → 启动秒开
第二层(.cache):单文件转译(TS/JSX)缓存 → 热更新快
第三层(内存):模块依赖图 → dev server 会话内复用

为什么依赖要预构建:

- CJS 依赖(老 npm 包)转成 ESM
- 上千个模块的依赖打散成少量 chunk → 浏览器请求数骤降
- 不透明依赖的转换成本一次付清,之后命中缓存

查看缓存目录:

ls node_modules/.vite/deps      # 预构建产物
ls node_modules/.cache/vite     # 转换缓存

记忆:三层缓存各管一段——依赖预构建(.vite)、转译(.cache)、模块图(内存);理解各自失效条件才能驾驭缓存。


2. 依赖预构建缓存:.vite 目录

依赖预构建(Optimize Dependencies) 的产物在 node_modules/.vite/deps/,是 Vite 启动快的最大功臣。

它做了什么:

- 扫描 import 的裸模块(react、lodash...)
- 用 esbuild 把 CJS → ESM、合并散装模块
- 产物带 hash:deps/react.js + hash 文件

命中/失效的判定依据:

失效(重新预构建)当:
  - package.json / lockfile 变化
  - optimizeDeps.include/exclude 配置变化
  - Vite 版本变化
  - 手动 --force(强制重新优化)
命中(直接用旧产物)当:
  - 依赖集合与配置都没变

手动控制:

npx vite --force        # 强制重新预构建(改依赖后保险)
npx vite optimize       # 手动预构建(提前做,而非首次访问触发)
// vite.config.js:显式声明要预构建的依赖
export default {
  optimizeDeps: {
    include: ["lodash-es", "axios"],
    exclude: ["@your-lib/private"],   // 已有 ESM 的不需要预构建
  },
};

注意:.vite 目录不应提交 Git(见第 7 节),但它可以进 CI 缓存(见第 6 节)。

记忆:预构建缓存是"启动快"的核心——失效看 package/lockfile/配置三件套,改依赖后 --force 保险,include 声明让首访不卡。


3. 缓存命中与失效判定

缓存三问:什么时候该命中?什么时候该失效?什么时候明明没变却不命中?

命中判定依据(常见实现):

- 依赖预构建:package.json + lockfile 的 hash
- 转换缓存:源码文件内容 hash
- 配置缓存:vite.config 的 hash

典型失效场景:

操作缓存影响
改 package.json 加依赖预构建缓存失效(要重新优化)
升级依赖版本失效(lockfile hash 变)
改 vite.config 的 optimizeDeps失效
改业务源码只影响转换缓存/模块图,不碰预构建
切分支依赖可能变 → 缓存未必命中

“没变却不命中"的常见原因:

- lockfile 有换行/排序抖动 → hash 变 → 全量重来
- 缓存目录被 CI 清理策略删除
- 切了 Node 版本 → 二进制缓存不兼容
- 缓存 key 没把"平台"算进去(CI 缓存跨 OS)

工程原则:缓存 key 要"精细但不脆弱”——依赖用 lockfile hash(稳定)、源码用内容 hash(精确)、平台纳入 key(跨机安全)。

记忆:缓存失效跟着"依赖/配置/平台"走——lockfile 稳定则命中、内容 hash 精确则正确;‘没变却不命中’九成是 lockfile 抖动或 key 没带平台。


4. 开发缓存 vs 生产构建缓存

两个"缓存"语境要分开:

语境缓存什么生效
vite dev预构建 + 转换 + 模块图进程内 + .vite/.cache
vite buildRollup 打包每次独立执行,默认无持久缓存

vite build 的现实:

- 默认每次都重新执行"转换 + 打包"
- 没有 webpack 那样的持久化构建缓存(disk cache)
- 要增量/缓存构建 → 靠外部缓存(第 5、6 节)或换工具

这就是为什么:vite build 在生产 CI 里每次都全量——除非你主动配置缓存(这正是 /vite-ci-cd-optimization/ 讲的核心)。

开发 vs 构建的衔接:

dev 缓存的产物(.vite)与 build 无关 → build 不会复用 dev 的预构建
build 有自己的依赖处理(Rollup 处理 node_modules)
→ 别指望"我 dev 跑过 build 就快"

记忆:dev 有强缓存、build 默认无持久缓存——‘build 快’要靠 CI 缓存/增量工具自己搭,别指望 dev 缓存传导。


5. 生产构建的增量难题

为什么 Vite build 不做增量:Vite 追求"每次构建的确定性"——不缓存跨进程状态,避免"缓存依赖的缓存"问题(正确性优先于速度)。

但工程需要增量,三招:

① 缓存依赖安装(node_modules 持久化)→ 装依赖秒回
② 缓存预构建产物(.vite)→ 冷启动依赖优化跳过
③ 用支持增量/缓存的替代(Turbopack 正在做持久缓存)

① node_modules 缓存(最实在):

- name: 缓存 node_modules
  uses: actions/cache@v4
  with:
    path: node_modules
    key: deps-${{ hashFiles('pnpm-lock.yaml') }}

② .vite 缓存:

- name: 缓存 Vite deps
  uses: actions/cache@v4
  with:
    path: node_modules/.vite
    key: vite-deps-${{ hashFiles('pnpm-lock.yaml') }}

③ 增量工具的现状:Turbopack(Next.js 底层)已支持持久化缓存;Vite 本身暂无官方持久缓存——期待 vs 现实的差,用 CI 缓存补齐。

记忆:build 增量三招——缓存 node_modules、缓存 .vite、关注 Turbopack 类增量工具;Vite 官方暂无持久缓存,用 CI 缓存做工程补位。


6. CI 持久化缓存:从零到命中

CI 缓存的完整链条(GitHub Actions):

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4

      - name: 缓存依赖(node_modules)
        uses: actions/cache@v4
        with:
          path: |
            node_modules
            node_modules/.vite
          key: ${{ runner.os }}-deps-${{ hashFiles('pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-deps-

      - name: 安装依赖
        run: pnpm install --frozen-lockfile

      - name: 构建
        run: pnpm build

缓存 key 设计:

key: ${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: 上一级通配 → lockfile 微变也能复用旧缓存大部分

命中率优化:

- lockfile 保持稳定(别让无关提交动它)
- 把 node_modules 与 .vite 分开缓存(一个失效不影响另一个)
- 平台入 key(linux/darwin/win 分开)
- 缓存上限管理:大缓存要定期清(prune)

验证命中:

- name: 检查缓存命中
  run: test -d node_modules/.vite/deps && echo "vite cache HIT" || echo "vite cache MISS"

记忆:CI 缓存三件——node_modules 与 .vite 分开缓存、key 用 lockfile hash + 平台、restore-keys 兜底;命中与否用 test 验证。


7. 缓存安全:不要把缓存提交进 Git

.gitignore 必加:

node_modules/
node_modules/.vite/
node_modules/.cache/
dist/

缓存进 Git 的危害:

- 仓库体积爆炸(.vite 有依赖的优化产物)
- 跨平台产物提交 → CI 上"命中奇怪错误"(平台差异)
- 缓存随仓库历史锁定 → 失效逻辑错乱
- PR 噪声:每次依赖变化 diff 一堆二进制

检查是否误提交:

git ls-files | grep -E '\.vite|\.cache' | head
# 若有输出 → 从 Git 移除
git rm -r --cached node_modules/.vite

注意:.vite 进 CI 缓存(actions/cache)是对的,进 Git 仓库是错的——缓存对象≠版本控制对象。

记忆:缓存进 CI 缓存不进 Git——.vite/.cache 必须 .gitignore;误提交用 git rm -r --cached 移除。


8. 常见缓存坑与排查

现象原因解法
改依赖不生效预构建缓存命中旧版本--force 或清 .vite
启动就全量重优化.vite 被删/CI 未缓存恢复缓存或 optimizeDeps.include
dev 热更新很慢转换缓存被清检查 .cache 是否被 CI 清理
build 每次一样慢无持久缓存上 CI 缓存/增量方案
切分支后缓存错乱依赖集合跨分支变分支差异化缓存 key
缓存目录在 Git仓库膨胀git rm -r --cached

排查命令:

# 强制重新预构建(先试这个)
npx vite --force

# 手动清缓存
rm -rf node_modules/.vite node_modules/.cache

# 查看预构建产物时间戳是否最新
ls -lt node_modules/.vite/deps | head

# 构建时打印缓存相关日志(-d 调试)
npx vite build -d

记忆:缓存坑多为’命中了旧的/没命中该中的’——--force 先试、清 .vite 兜底、CI 缓存分开配;排查从时间戳与日志入手。


9. 缓存治理清单

一套缓存治理清单:

□ .gitignore 已排除 .vite/.cache/dist
□ CI 缓存:node_modules 与 .vite 分开、key 含 lockfile + 平台
□ restore-keys 兜底(lockfile 微变可复用)
□ 改依赖后 --force 重新预构建(本地)
□ optimizeDeps.include 声明关键依赖(减少首访卡顿)
□ lockfile 稳定(避免缓存 key 抖动全量重来)
□ 生产 build 期望管理好(Vite 无持久缓存,靠 CI 补)
□ 定期检查缓存命中率(CI 日志)

一句话:缓存三件套——分层理解(预构建/转译/模块图)、key 设计(lockfile + 平台)、位置正确(CI 缓存而非 Git);治理目标是用最小成本拿到最高命中率。

记忆:缓存治理是"理解失效 + 设计 key + 摆正位置"——分层看清谁管什么、key 稳定可复用、CI 缓存而非 Git;照清单过一遍,构建缓存就从玄学变工程。


10. 速查表与一句话记忆

层位置失效条件
依赖预构建node_modules/.vitepackage/lockfile/配置
转译缓存node_modules/.cache源码内容
内存模块图dev 进程内进程重启
场景做法
改依赖不生效npx vite --force
启动全量重优化include 声明 / CI 缓存 .vite
build 加速CI 缓存 node_modules + .vite
缓存 key${{ runner.os }}-${{ hashFiles('lock') }}
Git 误提交缓存git rm -r --cached
增量构建期待 Turbopack 类工具 / 缓存补位

一句话记忆:Vite 缓存三层——预构建(.vite)管启动快、转译(.cache)管热更新、内存模块图管会话;dev 强缓存但 build 默认无持久缓存;失效跟着 package/lockfile/配置走、‘没变却不命中’看 lockfile 抖动与平台 key;CI 里 node_modules 与 .vite 分开缓存、restore-keys 兜底、缓存进 CI 不进 Git;改依赖 --force、排查看时间戳——缓存治理=理解分层+设计 key+摆正位置,构建快是工程不是魔法。


延伸阅读

  • /vite-dependency-pre-bundling/ — 依赖预构建机制深入
  • /vite-ci-cd-optimization/ — CI 缓存与并行构建实战
  • /vite-hmr-internals/ — 模块图与内存缓存
  • /vite-build-optimization/ — 构建产物与性能
  • [[devops]] — CI 缓存最佳实践
  • [[typescript]] — 构建与类型缓存

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「vite」更多文章

  1. Vite 静态资源与媒体资产处理:图片、字体、SVG 与 Worker
  2. Vite 环境变量与生产构建最佳实践:import.meta.env、构建模式与产物优化
  3. Vite 浏览器兼容与 Legacy 构建:build.target、Polyfill 与兼容插件