前端性能优化项目复盘:Lighthouse评分从45到95的系统性治理经验
一、性能债的量化起点
一个Vue3电商项目在Lighthouse评测中的初始状态:
- Performance: 45分
- First Contentful Paint (FCP): 3.2s
- Largest Contentful Paint (LCP): 5.8s
- Total Blocking Time (TBT): 820ms
- Cumulative Layout Shift (CLS): 0.25
这不是一个Bug——这是一整套系统性问题的集合。需要的是分阶段治理,而非一个"银弹修复"。
二、四个阶段的详细治理
阶段一:资源瘦身(LCP从5.8s到3.2s)
用Webpack Bundle Analyzer分析产物,发现三个主要问题:
- 首屏引入了完整版的ECharts(1.2MB)——但首页只用了简单的折线图。改为动态导入 + 按需加载,首屏ECharts体积从1.2MB降到280KB。
- 图片未压缩,一张Banner图4.2MB。引入自动化图片优化管道:
// vite.config.js import viteImagemin from 'vite-plugin-imagemin' export default { plugins: [ viteImagemin({ gifsicle: { optimizationLevel: 7 }, mozjpeg: { quality: 80 }, pngquant: { quality: [0.7, 0.8] }, webp: { quality: 80 }, // 生成WebP替代 }), ], }- 第三方库全量引入——Moment.js的locale文件全部打包(~300KB无用的多语言数据)。替换为dayjs(2KB)或配置Moment只引入需要的locale。
阶段二:渲染路径优化(FCP从3.2s到1.5s)
首屏渲染的核心问题是关键资源链太长:
HTML → CSS文件 → 字体文件 → JS Bundle → API请求 → 渲染
做了四个优化:
- 关键CSS内联:提取首屏必需的CSS(约3KB),内联到
<head>的<style>标签中。避免CSS文件阻塞渲染。 - 资源预加载提示:
<link rel="preconnect" href="https://api.example.com"> <link rel="preload" href="/fonts/main.woff2" as="font" crossorigin> <link rel="preload" href="/hero.webp" as="image">- 字体加载策略:
font-display: swap防止FOIT(Flash of Invisible Text),同时预加载字体文件。 - API请求提前:在HTML中嵌入首屏数据的预取脚本,减少"JS加载→执行→发请求"的等待链。
阶段三:运行时优化(TBT从820ms到80ms)
TBT高的根因是长任务(Long Task > 50ms)阻塞主线程。排查发现三个主要长任务:
- 首页的复杂计算——商品价格排序+筛选。移到Web Worker:
// worker.ts self.onmessage = (e: MessageEvent<{ products: Product[]; filter: Filter }>) => { const { products, filter } = e.data; const filtered = products .filter(p => p.price >= filter.min && p.price <= filter.max) .sort((a, b) => a.price - b.price); self.postMessage(filtered); }; // 主线程 const worker = new Worker(new URL('./worker.ts', import.meta.url)); worker.postMessage({ products, filter }); worker.onmessage = (e) => { this.filteredProducts = e.data; // 结果回来后才更新UI };- 首页几十个watcher同时触发——合并为computed + 单一watch。
- 滚动事件中的复杂计算——加入
requestAnimationFrame节流。
阶段四:布局稳定性(CLS从0.25到0.02)
CLS问题本质是"未知尺寸的内容"在渲染后撑开/收缩布局:
- 图片未声明尺寸 → 加载后突然撑开 → 如何修复:
<!-- 声明宽高比,浏览器预留空间 --> <img src="product.jpg" width="300" height="200" style="aspect-ratio: 3/2; width: 100%; height: auto;">- 动态内容(如广告位)异步加载后插入 → 预留固定高度的容器。
- Web字体加载导致文字大小变化 → 使用
size-adjust让fallback字体与Web字体尺寸一致。
三、持续性能监控
优化不是一次性的,需要持续监控:
// 前端上报Web Vitals import { onLCP, onFID, onCLS, onINP } from 'web-vitals'; function sendToAnalytics({ name, value, id, delta }) { const body = JSON.stringify({ name, value, id, delta, url: location.href, timestamp: Date.now(), }); // 使用sendBeacon确保页面关闭时也能发送 if (navigator.sendBeacon) { navigator.sendBeacon('/api/vitals', body); } } onLCP(sendToAnalytics); onCLS(sendToAnalytics); onINP(sendToAnalytics);在Grafana中建立Dashboard,按P50/P75/P95展示各项指标的趋势。设置告警:P95 LCP > 3s触发告警。
四、投入产出分析
| 优化阶段 | 耗时 | LCP改善 | 关键操作 |
|---|---|---|---|
| 资源瘦身 | 1周 | 2.6s | 图片压缩+Tree Shaking |
| 渲染优化 | 1周 | 1.7s | 关键CSS+预加载 |
| 运行时优化 | 2周 | 0.3s | Web Worker+长任务拆分 |
| 布局稳定 | 3天 | - | 图片尺寸+字体策略 |
总计约5周(一个开发者50%的时间),Lighthouse从45提到95。从业务数据看:页面加载时间降低53%,跳出率下降11%,转化率提升6%。
五、总结
从45到95的性能优化,方法论比具体技巧更重要:
- 先量后治。用Web Vitals数据驱动,每次优化有明确的指标变化,不凭感觉。
- 分阶段治理。先解决LCP(最大的问题),再FCP,再TBT,最后CLS。同时解决所有问题等于什么都没解决。
- 资源是最大的杠杆。图片压缩+Code Splitting占了LCP优化的70%以上收益。
- 持续监控是防止退化的唯一手段。GitHub CI中的Lighthouse检查,确保每个PR不会降分。
- Web Worker对TBT是性价比最高的优化。把计算移到后台线程,主线程专用于渲染。
最大的教训:性能优化不是"优化一次就完事",而是"建立监控→发现问题→优化→监控"的循环。没有Web Vitals监控的性能优化,就像没有测试的代码重构。