引言
「构建很快」不等于「用户打开很快」。Vite 负责把代码变成产物,而用户感知的性能由加载策略、缓存、资源优先级与运行时开销共同决定。本文聚焦运行期:产物如何被浏览器更快地加载与执行——代码分割与按路由懒加载、preload 资源优先级、长缓存策略、LCP/INP 的优化路径、性能预算与 RUM 监控。目标是把「构建期快」翻译成「首屏快、交互顺、缓存命中高」的用户体验。
前置:https://plumephp.com/vite-build-optimization/(构建期 chunk/压缩)、https://plumephp.com/vite-env-production-best-practices/(生产配置)、https://plumephp.com/vite-containerized-docker-builds/(缓存与部署)。
目录
- 1. 构建期与运行期的分工
- 2. 代码分割与按需加载
- 3. preload 与资源优先级
- 4. 静态资源缓存策略
- 5. LCP 优化路径
- 6. INP 与交互性能
- 7. 体积与压缩
- 8. 性能预算与 CI 门禁
- 9. RUM 测量与监控
- 10. 速查表与一句话记忆
- 延伸阅读
1. 构建期与运行期的分工
先分清「谁负责什么」,避免在错误的阶段优化:
| 阶段 | 手段 | 影响 |
|---|---|---|
| 构建期 | 代码分割、压缩、tree-shaking | 产物「多大、几个包」 |
| 运行期 | 加载顺序、缓存、优先级 | 用户「多快看到、多顺交互」 |
理想链路:
构建:产物体积小 + chunk 清晰
部署:CDN + 长缓存 + gzip/brotli
运行:按需加载 + preload 关键资源 + 交互零阻塞
核心认知:代码大小决定「下载多少」,加载策略决定「先下载什么、什么时候下载」。两者都要管,但先修「该懒加载的没懒加载」,比纠结几 KB 更有用。
2. 代码分割与按需加载
Vite 的 build.rollupOptions.output.manualChunks 与路由级懒加载配合,让「首屏只需首屏代码」:
// 路由级懒加载(React)
const About = React.lazy(() => import("./pages/About"));
// Vite 会把 About 拆成独立 chunk,仅在访问时下载
// 函数级懒加载(工具类大库)
const heavyUtil = (await import("./heavy-util")).default;
// vite.config.ts:手动拆 chunk,把稳定大库隔离
build: {
rollupOptions: {
output: {
manualChunks: {
react: ["react", "react-dom"],
vendor: ["lodash", "axios"],
},
},
},
}
运行期要点:
- 首屏只加载入口 + 关键 chunk:路由级懒加载默认生效;
- 共享依赖进独立 chunk:多个页面共用的大库(react/vendor)避免重复下载;
- 懒加载边界要稳:不要「按文件过度切」导致几十个 1KB chunk(HTTP 开销反而大)。
3. preload 与资源优先级
浏览器下载有优先级,preload/prefetch 是「告诉浏览器提前做什么」:
| 指令 | 含义 | 适用 |
|---|---|---|
<link rel="preload"> | 当前页面立即需要,提前下载 | 首屏关键 JS/CSS/图片/字体 |
<link rel="prefetch"> | 未来可能用,空闲时下载 | 下一页面的 chunk |
<link rel="preconnect"> | 提前建立连接 | 第三方域(API/CDN) |
<!-- preload 关键资源 -->
<link rel="preload" as="script" href="/assets/main-abc123.js">
<link rel="preload" as="font" type="font/woff2" crossorigin href="/fonts/xx.woff2">
<!-- prefetch 下一路由 chunk(基于路由预测) -->
<link rel="prefetch" href="/assets/about-def456.js">
// Vite 的 build.modulePreload:自动给入口 chunk 生成 preload
build: {
modulePreload: { polyfill: false },
}
工程要点:
- preload 要克制:预加载「关键路径」资源,不是预加载一切(反而挤占带宽);
- 字体/首屏图 preload:LCP 优化的直接杠杆(见第 5 节);
- prefetch 基于数据:用路由访问统计/用户行为预测下一页,避免盲 prefetch。
4. 静态资源缓存策略
缓存是「最便宜的性能」——命中了就不下载。Vite 产物的缓存设计:
带 hash 的资源(/assets/main-abc123.js):
→ 内容变则文件名变 → 可长缓存(immutable, 1y)
→ 文件没变,浏览器直接用缓存
不带 hash 的(index.html):
→ 每次协商(no-cache 或 ETag),保证新版本快速生效
# 示例(nginx)
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
add_header Cache-Control "no-cache"; # index.html 每次校验
}
运行期要点:
- hash 文件名是缓存命中的前提:Vite 默认对带内容的产物加 hash;
- service worker:PWA 场景可再加一层离线缓存(配合 manifest);
- CDN 缓存:边缘缓存静态资源,TTL 对齐「hash 不变」原则。
5. LCP 优化路径
LCP(Largest Contentful Paint) 衡量「最大内容何时出现」,是首屏体验的核心指标。Vite 项目的优化路径:
LCP 构成:发现 → 下载 → 解码/解析 → 渲染
优化:
□ 发现:preload LCP 资源(主图/首屏 JS/字体)
□ 下载:减少体积(压缩/去重),CDN 就近
□ 解析:减少主线程阻塞(首屏 JS 精简)
□ 渲染:字体 font-display: swap,图片尺寸预留
/* 图片尺寸预留:防止 LCP 元素布局抖动 */
img { width: 640px; height: 360px; }
/* 字体异步加载:不阻塞文本渲染 */
@font-face { font-display: swap; }
工程要点:
- 首屏体积预算:首屏 JS 建议 < 200KB(gzip);LCP 图用响应式尺寸 + 预加载;
- 避免大 JS 阻塞:路由懒加载 + 关键 JS 优先,把非关键脚本延后;
- 测量真实场景:LCP 在不同网络下差异大,用「慢速网络」预算为准。
6. INP 与交互性能
INP(Interaction to Next Paint) 衡量「交互后多久有响应」,是交互体验的指标。前端运行时优化:
INP 优化路径:
□ 减少主线程长任务(>50ms 的任务拆分)
□ 事件处理去重/节流(高频事件)
□ 避免昂贵的重新渲染(状态粒度、memo)
□ 懒加载非交互必需代码(减少初始 JS 执行)
□ 输入处理用 requestIdleCallback 降级
// 高频事件节流,避免主线程被刷爆
window.addEventListener("scroll", throttle(() => {
updateUI();
}, 16), { passive: true });
工程要点:
- JS 执行时间是 INP 的主要敌人:
Long Tasks监控(PerformanceObserver); - 减少初始 bundle 执行:非首屏逻辑拆到懒加载;
- 交互路径瘦身:点击 → 更新 UI 的路径上避免同步重计算。
7. 体积与压缩
体积是「所有性能指标的地基」:
压缩层次:
□ 代码压缩(Vite 默认 esbuild/terser 压缩)
□ gzip(~65% 压缩率)/ brotli(~70%+)
□ 预压缩产物(构建时生成 .gz/.br,服务器直接喂)
□ 去重与 tree-shaking(消除未用代码)
□ 依赖瘦身(只引子函数:lodash → lodash-es 按需)
// 按需引入(避免整个 lodash 进 bundle)
import debounce from "lodash-es/debounce";
// 或配置 vite 别名只引按需路径
# 预压缩(nginx 直接服务压缩产物)
for f in dist/assets/*.js dist/assets/*.css; do
gzip -9 -k "$f" # 生成 .gz
done
工程要点:
- 体积进 CI 报告:产物总大小 / 首屏 chunk 大小 / gzip 后大小,超预算失败(见下节);
- brotli 优先:CDN 支持时 brotli 压缩率更高;
- Tree-shaking 前提:用 ESM 库(
module字段),避免 CJS 干扰。
8. 性能预算与 CI 门禁
没有预算,性能优化是「一次性运动」;有预算,它是「持续门禁」:
// 性能预算(示例):
{
"首屏 JS ≤ 200KB (gzip)",
"产物总量 ≤ 800KB (gzip)",
"单 chunk 最大 ≤ 400KB (gzip)",
"LCP 中位数 ≤ 2.5s(RUM 实测)",
"INP 中位数 ≤ 200ms"
}
CI 门禁:
□ 构建后统计 bundle 体积 → 超预算失败
□ bundle-buddy/size-limit 等工具接入
□ RUM 指标按版本对比(回归即告警)
工程要点:预算要「绑定到构建产物」(可自动度量),RUM 指标(LCP/INP)作为「线上真相」——两者都对,性能才可持续。
9. RUM 测量与监控
优化闭环的最后一步是「测量真实用户」:
// RUM(Real User Monitoring):上报核心指标
import { onLCP, onINP, onCLS } from "web-vitals";
onLCP((metric) => report("web-vitals", metric));
onINP((metric) => report("web-vitals", metric));
onCLS((metric) => report("web-vitals", metric));
监控闭环:
采集(web-vitals)→ 上报(分析平台)→ 分位/分版本
→ 对比预算 → 告警 → 定位(source map)→ 优化 → 验证
工程要点:
- web-vitals 库是标准采集工具(Google 维护);
- 分位而非平均:LCP 看 p75/p90,排除长尾噪音;
- 版本对比:新版本上线对比旧版本核心指标,回归即拦(配合 https://plumephp.com/vite-sourcemap-deep-dive/ 定位)。
10. 速查表与一句话记忆
| 问题 | 一句话答案 |
|---|---|
| 构建期 vs 运行期 | 前者定体积,后者定加载顺序与缓存 |
| 怎么按需加载 | 路由级懒加载 + manualChunks 拆分 |
| 怎么控制优先级 | preload 关键 / prefetch 未来 |
| 缓存怎么设计 | hash 资源长缓存,index.html 协商 |
| LCP 怎么优化 | preload 主图/字体 + 首屏体积预算 |
| INP 怎么优化 | 减少长任务 + 事件节流 + 首屏 JS 精简 |
| 怎么持续不回归 | 体积/指标预算 + CI 门禁 + RUM |
一句话记忆:运行时性能 = 按需加载(懒加载/手动拆块)+ 优先级(preload/prefetch)+ 缓存(hash 长缓存)+ 指标优化(LCP 首屏/INP 交互)+ 预算门禁(体积/RUM)——把「构建快」翻译成「用户快」。
延伸阅读
- https://plumephp.com/vite-build-optimization/ — 构建期 chunk/压缩/tree-shaking
- https://plumephp.com/vite-env-production-best-practices/ — 生产构建与 CDN
- https://plumephp.com/vite-asset-processing/ — 静态资源与媒体处理
- https://plumephp.com/vite-containerized-docker-builds/ — 部署层的缓存与压缩
- https://plumephp.com/vite-sourcemap-deep-dive/ — 用 source map 定位线上性能问题
- 前端专题 — 前端性能与工程化
- 网络专题 — 缓存、CDN 与 HTTP
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。