引言
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 缓存的三层结构
- 2. 依赖预构建缓存:.vite 目录
- 3. 缓存命中与失效判定
- 4. 开发缓存 vs 生产构建缓存
- 5. 生产构建的增量难题
- 6. CI 持久化缓存:从零到命中
- 7. 缓存安全:不要把缓存提交进 Git
- 8. 常见缓存坑与排查
- 9. 缓存治理清单
- 10. 速查表与一句话记忆
- 延伸阅读
1. Vite 缓存的三层结构
Vite 的缓存分三个层级,各管一段:
| 层 | 位置 | 内容 | 生命周期 |
|---|---|---|---|
| 依赖预构建 | node_modules/.vite | 依赖的 esbuild 优化产物 | 随 package.json 变化失效 |
| 转换缓存 | node_modules/.cache | esbuild 转译结果 | 源码变化失效 |
| 内存模块图 | 内存 | 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 build | Rollup 打包 | 每次独立执行,默认无持久缓存 |
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/.vite | package/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]] — 构建与类型缓存
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。