Vue 3 + Vite 项目实战:Lighthouse 100 分优化指南

📅 发布时间:2026/10/18 0:58:43
Vue 3 + Vite 项目实战:Lighthouse 100 分优化指南
前阵子我把一个 Vue 3 Vite 的项目从头到尾折腾了一遍目标很明确让 Lighthouse 跑出 100 分。这个项目不算大但包含路由懒加载、数据请求、图表展示、国际化切换等常见功能足够代表大多数中后台应用。经过几轮优化我确实把 Performance、Accessibility、Best Practices、SEO 四项全部拉满过程中踩了不少坑也总结出一套可以复用的打法。如果你也希望自己的 Vue 应用在审计工具里拿满分这篇文章就是给你准备的。Lighthouse 是一个开源审计工具跑完会给出四个维度的分数附带一堆改进建议。很多人觉得“100 分”只是数字游戏但在我实际优化后用户体验的改善是实打实的白屏时间更短、切换页面不卡、长列表滚动不掉帧。这篇文章不会只甩最后结论我会把项目搭建、指标拆解、构建配置、资源优化到常见问题的完整链路都讲清楚并把关键的操作命令和配置贴出来方便你直接上手复现。1. 为什么把 Lighthouse 100 分定为优化终点1.1 Lighthouse 到底在测什么Lighthouse 的审计结果分为四个大块Performance性能、Accessibility可访问性、Best Practices最佳实践、SEO。每一块都有一个百分比分数但它们的计算方式和权重完全不同。Performance 的分数主要由六个核心指标决定FCPFirst Contentful Paint、LCPLargest Contentful Paint、TBTTotal Blocking Time、CLSCumulative Layout Shift、SISpeed Index、TTITime to Interactive。其中 FCP、LCP、CLS 是“现场指标”的替代项TBT 和 TTI 反映交互响应性SI 是视觉完成速度。Lighthouse 会在移动端模拟 4 倍 CPU 降频和 1.6 倍网络延迟所以你在本地跑出来的分数往往比真实低配手机上的体验更苛刻。这其实是个好事拿高分意味着你的应用在正常设备上会更快。Accessibility 和 SEO 的审计项更多是“布尔型”判断比如有没有替代文本、标题层级是否正确、链接有没有描述性文字。这类问题通常改一处就过不像性能指标需要长期调优。Best Practices 则是一些安全和现代性检查比如是否使用 HTTPS、控制台有没有报错、图片是否有正确宽高。四个维度加起来基本上能勾勒出一个前端应用的健康度全貌。1.2 100 分不等于绝对完美先说句实在话Lighthouse 100 分不代表你的应用“没有优化空间了”也不代表网站体验已经天下无敌。它只是说明在审计工具定义的标准场景下你的页面达标了。比如有些高频且不影响分数的优化项Lighthouse 不一定覆盖有些资源体积削减措施在低速网络下有效但在测试环境看不出来。所以我更愿意把 100 分当成一个可量化的里程碑而不是终点。另一个常见的误区是“为了 100 分把工程搞复杂”。我看到有人为了把 LCP 压到 1 秒把首屏所有组件都动态 import结果首屏虽然快但用户点击按钮后等了半天才加载出弹窗交互体验反而更差。性能优化的原则永远是“用户体验优先”分数只是辅助判断的工具。我会在每项优化里强调它和实际体验的关联避免你陷入刷分陷阱。2. 搭建可复现的 Vue 3 Vite 基准项目2.1 初始化项目与依赖为了不让优化结论失真我通常会新建一个独立的测试项目尽量模拟真实业务结构再逐步优化。这次我用的是 Node 20 和最新版 Vite。初始化命令很简单npm create vitelatest vue-perf-guide -- --template vue cd vue-perf-guide npm install这里有两个细节值得注意一是包管理器我用的是 npm如果你用 pnpm原理一样但锁文件不同后续安装依赖时容易出现版本分歧二是模板类型我选的是 Vue 官方模板不带 TypeScript因为后面优化时改配置更直观。实际项目如果是 TypeScript所有优化手段同样适用只是额外成本会多一些。为了让项目更接近真实应用我加了两条关键依赖vue-router 和 pinia。前者用来做路由懒加载验证后者用来管理全局状态。安装命令npm install vue-router4 pinia随后在 src 下创建了三个页面首页、列表页、详情页。首页放一个大型表格和几张图片列表页放卡片列表详情页放富文本内容。这就是后续优化要对抗的“重量级资源”。2.2 Lighthouse 测试环境与姿势Lighthouse 的测试方式会直接影响分数姿势错了100 分就是水中月。我强烈建议使用 Chrome 的独立 Lighthouse 面板而不是某些在线测速网站因为本地跑可以精确控制节流条件和存储状态。具体操作打开 Chrome DevTools切到 Lighthouse 标签在“Mode”里选“Navigation”在“Device”里选“Mobile”然后勾选 Performance、Accessibility、Best Practices、SEO 四个类别。跑了第一次之后你会发现分数可能在七十分上下先别急着优化先把测试变量固定下来。固定变量是关键。同一台电脑同样开着开发者工具如果一边后台在跑视频渲染一边测 Lighthouse分数至少差十几分。我一般会做三件事关掉无关标签页、把显示器分辨率固定、用无痕窗口进入页面。无痕模式可以排除扩展和缓存干扰尤其像某些浏览器插件会注入脚本直接拉低性能和最佳实践分数。为了避免缓存带来的假高分每次测量前我都会在 Lighthouse 设置里勾选“Clear storage”选项。也就是说每次审计都相当于新用户第一次访问DNS 要重新解析、TCP 要重新握手、所有静态资源都要重新下载。这是最接近首访体验的测试方式也是爬向 100 分的正确起点。3. 性能指标逐个击破满分的关键路径3.1 从 FCP 到 LCP首屏渲染优化Performance 满分的第一步是把 FCP 和 LCP 压下去。FCP 是页面第一个文本或图片绘制的时间LCP 是首屏最大内容绘制完成的时间。在 Vue 3 Vite 项目中影响这两个指标的主要有三样东西入口 JS 的大小、阻塞渲染的同步脚本、以及首屏图片的加载方式。入口 JS 最大的问题在于它包含了整个应用的路由、组件、状态管理代码。Vite 默认会把所有 JavaScript 打成一个 bundle如果没有做拆分首屏就要下载几百 KB执行还要额外耗时。解决办法是路由懒加载把每个页面变成独立的 chunk。改造方法很简单import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: home, component: () import(../views/HomeView.vue) }, { path: /list, name: list, component: () import(../views/ListView.vue) }, { path: /detail, name: detail, component: () import(../views/DetailView.vue) } ] export default createRouter({ history: createWebHistory(), routes })这样配置之后首页只会加载首页自己的 JS 和公共依赖。Vite 还会把 node_modules 里的依赖单独打成 vendor chunk公共依赖被缓存后切换页面只需要加载几十 KB 的页面代码。在我的项目里首屏 JS 从原来的 580 KB 降到了 190 KBFCP 直接快了一倍。除了代码体积内联样式和阻塞脚本也是 LCP 的大敌。Vite 默认生成的是纯外链样式表渲染时要等样式下载完才能绘制。如果你有一些首屏关键样式比如主体背景色、布局骨架可以把它内联进 index.html 的 style 标签里。这样浏览器无需额外发一次请求就能画出首屏背景视觉上明显更快。但别把所有样式都内联否则 HTML 体积会失控反而拖慢解析。3.2 TBT 与 TTI交互阻塞的消除TBT 衡量的是主线程被长任务阻塞的总时间TTI 是页面可完全交互的点。Vue 应用的主线程开销主要来自三块初始化 Vue 实例、挂载组件、执行事件绑定。如果首屏组件太重TBT 一定高。我遇到最典型的情况是一个包含大量表格数据和图表的分页组件它把所有数据渲染逻辑都同步执行了。优化方式是延迟非首屏关键组件的渲染。以图表为例用 Vue 3 的动态组件加条件渲染只在滚动到可视区域后才加载组件。你可以用 IntersectionObserver 实现一个简单的懒挂载指令也可以直接引入第三方懒加载容器组件。这里我更推荐后者因为社区组件通常已经处理了边界情况比如组件卸载时 observer 的清理。另一个隐藏的 TBT 杀手是全局状态初始化里做了大量计算。例如 Pinia 的 store 在模块加载时就会执行 setup如果你在 setup 里解析 JSON 或请求接口阻塞时间就悄悄变长了。正确做法是把非关键逻辑放进异步 action或者用 computed/lazy getter 延迟计算。我曾把一个 store 里的深拷贝操作从 setup 移到用户点击事件里执行TBT 从 680ms 降到了 180ms。3.3 CLS 的隐藏陷阱CLS 是布局偏移分数它和性能优化关系不大但却是最容易从满分里掉下来的一项。CLS 过高的常见原因有三个图片没有设置宽高、字体加载导致文本重排、异步注入的内容出现在已有内容上方。图片宽高的问题在 Vue 项目中非常普遍因为某些图片组件的 src 是动态拼接的前端在运行时才知道加载的图片比例。解决方法是给图片外层容器固定一个占位宽高比。Vue 3 里的做法可以是template div classimage-wrapper :style{ aspectRatio: 16 / 9 } img :srcsrc alt / /div /template字体导致的重排也不容小觑。如果使用了自定义字体且 CSS 里 font-display 是默认的 block那么字体下载完成前文字是用隐形占位的下载完再显示会挤开其他元素。把 font-display 设为 swap 可以解决大部分 CLS 问题但 swap 有时也会在字体未加载时先显示“错误的字体”造成视觉抖动。最稳妥的路线是使用 font-size-adjust 或者对字体做子集化只加载页面用到的字符这样字体体积小加载快重排窗口期也会短很多。至于异步内容注入导致的 CLS我通常建议所有异步插入的 DOM 都放在预留容器里或者先插入一个固定高度的骨架屏拿到数据后再替换。这样就算数据晚到页面也不会突然跳动。4. 资源加载与构建配置的精细化调优4.1 代码分割与路由懒加载前面提到路由懒加载能缩小首屏包但只看这些还不够。有些场合同一个路由里的不同组件也有明显的优先级差异比如详情页里的大图表和评论列表。这时可以在组件内再做动态 import让图表延迟加载script setup import { defineAsyncComponent, ref } from vue const ChartBox defineAsyncComponent(() import(../components/ChartBox.vue) ) const showChart ref(false) /script template div button clickshowChart true显示图表/button ChartBox v-ifshowChart / /div /templatedefineAsyncComponent 可以配合 Suspense 精确控制加载状态和错误状态而且不会把图表代码打进首屏 chunk。用这个方案后详情页的首屏 JS 又少了一百多 KBTTI 明显提前。还有一个容易忽略的拆分点第三方库。如果你的项目用了日期处理库、格式化库或工具函数集尽量通过按需导入或使用颗粒度更细的库来替代。Vite 构建时可以用 rollup 的 manualChunks 选项进一步拆分依赖让浏览器缓存更高效。比如把 echarts、lodash-es 拆成独立 chunk它们版本更新频率低可以被长期缓存。4.2 图片、字体与第三方资源优化图片优化往往是 Lighthouse 提升最明显的一步。在移动模拟下1.6 倍网络延迟会让一张 2MB 的 JPEG 拖慢 LCP 好几秒。建议做法所有图片转成 WebP 或 AVIF 格式用图片压缩工具把质量控制在 75 左右同时给 img 标签加上 fetchpriorityhigh 或 loadinglazy让首屏关键图优先加载非首屏图滚动到视野内再加载。我在项目里把首屏 Hero 图从 PNG 换成了 WebP体积从 1.8MB 降到 120KBFCP 和 LCP 各自都提升了约 0.8 秒。如果你的图片服务商支持自动转换格式那是最省事的路径。如果图片是前端静态资源推荐直接用 Vite 的 assetsInlineLimit 选项结合手动压缩工具处理。字体方面最常见的评级建议是“以文本形式显示无样式文本不阻塞渲染”。为此你需要给每个 font-face 声明加上 font-display: swap。这不仅能防 CLS还能避免因字体加载导致的渲染阻塞。更深层的优化是 font-display: optional它允许浏览器在极短时间内使用本地字体渲染如果网络字体没赶上就不再加载网络字体。这样首屏绘制就不必等待字体请求完成非常适合强调“首选帧速度”的场景。第三方资源是另一个看不见的坑。很多站点为了统计流量接入了分析脚本、埋点脚本这些脚本如果放在 head 里同步加载会直接拉高 TBT。解决办法是在 index.html 中把第三方脚本改成 async 或 defer并尽量放到 body 末尾。对于必须首屏执行的埋点建议通过动态注入 script 标签并在 onload 后执行。我在项目里把一个 150KB 的统计脚本改成延迟到空闲时间加载TBT 下降非常明显。4.3 Vite 构建配置的细节取舍Vite 本身已经很快但构建配置直接影响产物质量。我整理了一份经过验证的 vite.config.js 核心配置import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ build: { target: es2020, cssCodeSplit: true, sourcemap: false, rollupOptions: { output: { manualChunks: { vendor: [vue, vue-router, pinia], charts: [echarts], } } } } })target 设置为 es2020能让 Vite 输出更现代的语法减少转译带来的额外代码。sourcemap 关闭可以减小构建产出但注意在生产排错时会缺信息可以根据团队需求取舍。cssCodeSplit 保持默认开启即可这样每个路由只加载自己需要的样式。manualChunks 的配置也可以更进一步用函数形式按依赖包名自动分组避免所有第三方库挤在一个 vendor 里导致 chunk 过大。如果你发现某个第三方库体积很大但只有某个页面用建议不要塞进 manualChunks让它跟随页面代码单独拆开页面加载时才下载。还有一项容易被忽视的配置是 build.reportCompressedSize。我平时会打开它看一眼 Gzip 压缩后每个 chunk 的体积。Vite 默认只显示压缩前大小但实际浏览器下载的是压缩后体积两者可能差 70% 以上。举个例子一个 200KB 的 JS chunkGzip 后可能只有 50KBLighthouse 计算资源传输大小用的也是传输编码体积所以你需要关注的是压缩后尺寸。用npx vite build --watch配合报告插件可以直观地看到每个模块对总包体积的贡献再决定要不要做按需引入。5. 可访问性、最佳实践与 SEO 的满分补齐5.1 语义化结构与对比度修复Accessibility 满分往往比性能更容易达到但前提是你要养成写语义化标签的习惯。我在项目里把所有 div 改成了语义化标签header、main、nav、footer给表单控件配上明确 label给图标按钮加上 aria-label。还有一个容易漏的地方图片的 alt 不能乱写。如果一张图是装饰性的alt 应该留空字符串alt让读屏器跳过如果是内容图alt 要能描述图片内容。Lighthouse 的审计会检查每个 img 的 alt 属性是否存在以及是否有意义。对比度是另一个常见坑。浅色背景下灰色文字的对比度很可能低于 4.5:1审计直接扣分。我经常碰到设计师给的 UI 稿里按钮文字是 #999看起来高级但阅读性很差。解决方案是先全局搜一遍色值把所有正文和背景的对比度提升到 AA 标准。可以用浏览器开发者工具里的配色选择器计算对比度也可以直接修改主题变量。我这里用的是 CSS 变量统一管理:root { --text-primary: #1a1a1a; --text-secondary: #4d4d4d; --bg-primary: #ffffff; }这样一来改色值只需要动变量不会零散地影响组件。唯一要注意的是别为了高分把所有对比度都弄得黑白分明那样反而丧失视觉层次。Lighthouse 的 AA 标准是 4.5:1大号文本可以放宽到 3:1实用情况下按钮文字和占位符也要留足够对比。5.2 元信息、结构化数据与爬虫可见性SEO 审计项没有技术难度但细节多。每个页面需要独立的 title 和 meta description而且 title 要能反映页面内容的重点。Vue 是 SPA默认只有一个 index.html所以要用 vue-router 的 meta 字段或者专门的 SEO 库动态修改 document.title。我建议至少写一个小的 composableexport function usePageMeta(title: string, description: string) { document.title title const meta document.querySelector(meta[namedescription]) if (meta) { meta.setAttribute(content, description) } }然后在每个页面组件的 onMounted 里调用。这样搜索引擎在抓取时能看到不同的页面标题Lighthouse 的检查也能通过。再进一步如果你需要更好的 SEO 表现可以考虑使用服务端渲染或静态生成但在课程目标下动态 title 仍然有效。结构化数据也是 SEO 审计的一个加分点但 Lighthouse 不检查 JSON-LD我之所以提它是因为真实搜索引擎爬虫会把它当重要信号。在 index.html 或单独的 script 标签里加入符合实际场景的 JSON-LD比如组织、面包屑导航、文章信息能帮助爬虫理解页面结构。注意 JSON-LD 内容必须真实有效不要堆砌无意义的关键词否则可能被判定为作弊。除了结构化数据还要检查页面有没有 robots meta 标签、canonical 链接和语义化链接文本。像“点击这里”这种文本就是审计中的低分项应改成“查看Vue性能优化指南”。5.3 用自动化脚本兜底常见审计项手动检查这些可访问性和 SEO 项容易遗漏所以我写了一份小的检查清单脚本在构建前跑一遍。它不依赖任何真实特定工具核心逻辑是扫描 dist 目录里的 HTML 文件检查是否缺少 lang、meta、title以及 img 是否缺失 alt。你可以把它接入 package.json 的 CI 流程{ scripts: { check:seo: node scripts/check-seo.mjs } }这样每次构建后脚本会输出未通过的项目提醒你补漏。这个脚本不需要太复杂我用的是 Node 内置的 fs 和字符串匹配几行代码就能搞定。关键是养成立即修复的习惯否则等到 Lighthouse 跑出 90 分再去查哪个 img 没有 alt非常费劲。Best Practices 审计中有一点常被忽略浏览器扩展会干扰测试。当你用无痕模式时很多扩展停掉了但系统级代理或本地服务可能会被审计工具识别为不安全上下文。建议在本地跑审计时使用 localhost 环境同时确保页面没有控制台报错。Vue 开发模式下控制台会有 Vue 自己的 warning最好生产构建后再测。我会在 npm script 里加一条preview命令用vite preview启动生产产物的静态服务器然后对该地址跑 Lighthouse这样代表的是用户看到的真实线上表现。6. 实战复盘从 74 分到 100 分的完整记录6.1 基线诊断与问题清单我在初版项目上跑了一次 Lighthouse结果Performance 74、Accessibility 92、Best Practices 100、SEO 88。Performance 主因是 LCP 时间过高3.2 秒、TBT 达到 840msSEO 掉分是因为首页 title 重复且 meta description 缺失还有一个图片没有 alt。Accessibility 扣分来自对比度不足和若干表单控件缺少 label。把问题列成表格会更清楚维度基线分数主要问题Performance74首屏 JS 体积 580KB图片未压缩TBT 840msAccessibility92两处文字对比度不足两个 img 缺 altBest Practices100无SEO88缺少 meta descriptiontitle 重复得到问题清单之后我没有一次性全改而是按优先级分组先处理性能和图片再补 SEO最后修可访问性。因为性能问题修起来最容易反弹需要反复验证可访问性和 SEO 是“一次性修复”。6.2 优化动作和对应指标变化第一轮优化我先做了三件事配置路由懒加载、给首屏图片转 WebP、给自定义字体加 font-display: swap。改完后 Performance 从 74 涨到 88LCP 从 3.2 秒降到 1.8 秒TBT 降到 420ms。但 TBT 依然不满足满分条件因为我发现详情页的一个大列表组件在首屏也参与挂载于是给它加了异步懒挂载。这轮之后TBT 降到了 180msPerformance 到 97。第二轮优化针对剩下的 3 分。我把首页首屏的关键背景色内联到 index.html减少了样式表的到首帧绘制时间。同时把一个统计脚本从同步加载改为异步延迟加载LCP 降到 1.1 秒TBT 降到 90ms。最终 Performance 到达 100。SEO 和 Accessibility 的修复相对直接给首页、列表页、详情页分别设置了独立 title 和 description补上了两个 img 的 alt调整了次要文字的颜色变量。这之后四项分数全部为 100。需要说明的是Accessibility 和 SEO 的修复不会反向影响 Performance因为改动很小。6.3 分数波动与持续监控100 分不是一劳永逸的。当我把项目继续开发加了新的第三方库、改了几处组件分数可能会跌到 95 以下。所以我建立了一个简单的持续监控在 GitHub Actions 中每次 main 分支打 tag 后用 Lighthouse CI 跑一次审计并把结果推送到一个公开的 JSON 文件里。如果某次分数低于阈值CI 会失败并提醒团队。如果你在小团队或自己维护项目可持续监控也可以很简单每个月手动跑一次 Lighthouse把结果存档对比重点观察 LCP 和 TBT 的走势。依赖升级、组件增多、图片替换都会悄悄影响性能。最好的办法是在项目里放一个performance-budget.json设定各个 chunk 的体积上限。我用 Vite 的预算插件配置当某个 JS 文件超过 120KB构建直接失败从源头阻止性能回归。7. 常见问题与排查技巧实录7.1 Lighthouse 分数不稳定怎么办这是提问率最高的问题。分数波动可能来自三个方面测试环境不稳定、页面请求依赖网络、浏览器扩展或后台任务干扰。遇到波动我的第一步是在同一环境连续跑三次取中位数。如果三次数值差异不超过 3 分说明优化效果可信如果差异超过 5 分优先检查是不是有动态广告位、A/B 测试脚本或时间相关的逻辑。另一个不稳定来源是本地开发模式。dev server 的产物没有做压缩和混淆性能会比生产环境差很多Lighthouse 模拟时网络也慢甚至可能加载不了全部资源。所以测 Lighthouse 前务必用vite preview或部署到临时地址测试并且确保NODE_ENVproduction。很多人跑出非常低的分数都是因为测试了 dev server。7.2 优化后某指标不升反降我在优化过程中遇到过这种现象改了图片格式后LCP 反而变高了。后来发现新图片的尺寸虽然小但它的解码耗时更长或者加载优先级被浏览器降级了。解决办法是给首屏图片设置fetchpriorityhigh然后确认图片容器的aspect-ratio有正确占位。Lighthouse 的 LCP 计的是“最大内容绘制”的时间如果你把最大的图片延迟加载LCP 自然就延后了。类似的还有给组件加了懒加载之后TBT 降低了FCP 反而略涨。原因可能是动态 import 产生了一个小请求在弱网下会增加一次往返。这种情况下可以接受 TBT 降低带来的交互改善或者用 prefetch 把动态组件预加载让它不阻塞首屏但能被更快拉取。关键是要看整体用户体验而不是盯着单指标不平衡。7.3 实战踩坑清单分享几个我实际踩过的坑希望你能避开。第一个坑修改 meta description 时写到了 index.html 里但实际上每个路由切换时 SCAN 不会改变 meta。所以必须用 JavaScript 动态修改或者预渲染。我一开始只在首页加了 description审计通过但把详情页分享出去时缩略图和描述全部是首页的非常尴尬。第二个坑异步组件加 Suspense 边界后部分组件被提前卸载导致请求丢失。某些 require 动态 import 的组件在初始化时会立即发送请求如果用v-if控制加载组件尚未挂载时请求不会发出用户点开后才发现空白。解决办法不要在v-if和 Suspense 之间做过度控制用defineAsyncComponent的loadingComponent先展示一个骨架。第三个坑把外部图片直接放在 public 目录结果没有被构建工具压缩和转格式。public 里的文件会被原样复制到 dist任何优化都不生效。记得把所有需要优化的图片放进src/assets让 Vite 的 asset pipeline 处理它们。第四个坑在移动端模拟下字体子集化没做好导致 FCP 之后字体还在加载文本被 swap 闪了一下。解决方法是把字体文件转成 woff2并用unicode-range指定字体覆盖的字符集。如果还用不上所有汉字可以把字库切成几个子集只加载当前页面需要的部分。我这里用了一个开源字体分割脚本把 5MB 的字体文件分割成了多个 20KB 的碎片效果立竿见影。最后再说一个小技巧在跑 Lighthouse 之前可以先打开 Chrome 的“Network”面板和“Performance”面板测试一次看看哪些请求是等待最久的。Lighthouse 给出的瓶颈分析和 DevTools 的 waterfall 大多能对得上。如果你看到某个请求拖了很久先去优化它往往比盲目套用网上通用配置更有效。优化这件事永远是“对症下药”比“面面俱到”容易落地。