前端性能优化实战手册·第1篇:从 Lighthouse 60 到 95 的完整路径

《前端性能优化实战手册》系列 · 第 1 篇

本篇将用一份真实的 Lighthouse 审计报告为起点,带你走完从诊断到优化的全流程。每一个优化都有前后对比数据。

前言:性能优化的正确打开方式

很多团队的"性能优化"是这么做的:产品说要优化了,开发看看 Lighthouse 报告,做一轮图片压缩、加个懒加载、开个 gzip,分数涨了 10 分,收工。

但真正的性能优化应该是:量化诊断 → 确定瓶颈 → 逐项优化 → 数据验证 → 建立护栏

这个系列,我会带你用这套方法论,从零开始把一个 60 分的项目优化到 95 分。本篇是第 1 篇:聚焦核心指标诊断和最快见效的 5 大优化。


一、先读懂你的 Lighthouse 报告

1.1 五大核心指标(2026 版权重)

指标 全称 含义 你的目标 权重
LCP Largest Contentful Paint 最大内容绘制时间 ≤ 2.5s 25%
TBT Total Blocking Time 主线程阻塞总时间 ≤ 200ms 30%
CLS Cumulative Layout Shift 累计布局偏移 ≤ 0.1 25%
SI Speed Index 视觉速度指数 ≤ 3.4s 10%
FCP First Contentful Paint 首次内容绘制 ≤ 1.8s 10%

2026 年更新:INP(Interaction to Next Paint)已在 2024 年 3 月正式替代 FID 成为 Core Web Vital,但在 Lighthouse 综合评分中权重通过 TBT 体现。TBT 高的页面 INP 几乎必然差。

1.2 典型 60 分项目的「病历」

这里模拟一个真实的中型 React 项目初始 Lighthouse 报告:

复制代码
╔══════════════════════════════════╗
║  Lighthouse Performance:  60    ║
╠══════════════════════════╤═══════╣
║  FCP                     │ 3.2s  ║
║  LCP                     │ 5.8s  ║
║  TBT                     │ 820ms ║
║  CLS                     │ 0.35  ║
║  SI                      │ 6.1s  ║
╚══════════════════════════╧═══════╝

问题清单:

  1. 首屏加载了一个 1.2MB 的 vendor.js(包含 moment.js 全量 + lodash 全量)
  2. Hero Banner 是一张 2.3MB 的 PNG 图片,没有任何压缩和格式优化
  3. 谷歌分析和第三方客服 SDK 同步加载,阻塞渲染 800ms+
  4. 自定义字体 Noto Sans SC 全量加载,FOIT(Flash of Invisible Text)体验极差
  5. 首页渲染后布局抖动:图片加载完成前没有预留空间,CLS 爆炸

二、优化一:图片 ------ 投入产出比最高的优化

「先动刀子砍图片」是性能优化的第一铁律。因为在大多数项目中,图片占页面总大小的 60-70%。

2.1 现状诊断

复制代码
❌ Banner 图片: hero.png → 2.3MB
❌ 产品图片 × 8: 每张约 400KB → 共 3.2MB
❌ 图标: SVG 内联但未优化,重复路径
📊 图片总大小: 5.5MB,占页面总请求的 68%

2.2 优化措施

第一刀:格式转换(PNG → WebP + AVIF 降级)

html 复制代码
<!-- 优化前:一张 2.3MB 的 PNG -->
<img src="/images/hero.png" alt="Hero Banner" />

<!-- 优化后:AVIF 优先,WebP 降级,PNG 兜底 -->
<picture>
  <source srcset="/images/hero.avif" type="image/avif" />
  <source srcset="/images/hero.webp" type="image/webp" />
  <img 
    src="/images/hero.png" 
    alt="Hero Banner" 
    width="1200" 
    height="600"
    loading="eager" 
    fetchpriority="high"
  />
</picture>

第二刀:响应式图片

不要给移动端加载桌面端的 2x 图:

html 复制代码
<img 
  src="/images/product.jpg"
  srcset="
    /images/product-400.webp  400w,
    /images/product-800.webp  800w,
    /images/product-1200.webp 1200w
  "
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  alt="产品图"
  loading="lazy"
  decoding="async"
  width="400"
  height="300"
/>

decoding="async" 让浏览器在空闲时解码图片,不被图片解码阻塞主线程渲染。这个属性经常被忽略,但对 TBT 有直接改善。

第三刀:构建时自动处理

手工转格式不现实。在构建流程中自动化:

js 复制代码
// vite.config.js --- vite-plugin-imagemin 配置
import viteImagemin from 'vite-plugin-imagemin';

export default defineConfig({
  plugins: [
    viteImagemin({
      gifsicle: { optimizationLevel: 3 },
      mozjpeg: { quality: 80 },
      pngquant: { quality: [0.7, 0.9] },
      webp: { quality: 80 },
    }),
  ],
});

2.3 效果验证

指标 优化前 优化后
图片总大小 5.5MB 1.1MB ↓ 80%
LCP 5.8s 3.2s ↓ 45%
Lighthouse 分 60 72 ↑ 12

三、优化二:JavaScript ------ 砍掉最大的那块肉

3.1 现状诊断:分析你的 Bundle

第一步永远是打开 bundle analyzer,看清你在加载什么:

bash 复制代码
# Vite 项目
npx vite-bundle-analyzer

# Webpack 项目
npx webpack-bundle-analyzer dist/stats.json

典型发现:

复制代码
vendor.js:  1.2MB
├── moment.js         380KB  (已废弃,换成 dayjs 直接省掉)
├── lodash            270KB  (只用了 debounce 和 cloneDeep)
├── antd icons         220KB  (全量引入)
├── react-dom          130KB  (合理)
└── core-js             90KB  (合理)

3.2 优化措施

第一刀:moment.js → day.js

js 复制代码
// 优化前:全量引入 moment
import moment from 'moment';
moment().format('YYYY-MM-DD');

// 优化后:dayjs 按需引入
import dayjs from 'dayjs';
dayjs().format('YYYY-MM-DD');
// 省下 380KB,API 几乎完全兼容

第二刀:lodash → 按需引入 / 原生替代

js 复制代码
// 优化前:全量引入
import _ from 'lodash';
_.debounce(fn, 300);

// 优化后:按需引入
import debounce from 'lodash-es/debounce';
// 或直接用原生实现常见工具
// 省下 250KB

第三刀:路由级代码分割

jsx 复制代码
// 优化前:静态 import,所有路由代码打进一个 bundle
import Dashboard from './pages/Dashboard';
import Analytics from './pages/Analytics';
import Settings from './pages/Settings';

// 优化后:动态 import,每个路由独立 chunk
const Dashboard = lazy(() => import(
  /* webpackChunkName: "dashboard" */ './pages/Dashboard'
));
const Analytics = lazy(() => import(
  /* webpackChunkName: "analytics" */ './pages/Analytics'
));
const Settings = lazy(() => import(
  /* webpackChunkName: "settings" */ './pages/Settings'
));

// 配合 Suspense 边界
<Suspense fallback={<PageSkeleton />}>
  <Routes>
    <Route path="/dashboard" element={<Dashboard />} />
    <Route path="/analytics" element={<Analytics />} />
    <Route path="/settings" element={<Settings />} />
  </Routes>
</Suspense>

第四刀:第三方脚本异步化

html 复制代码
<!-- 优化前:同步加载,阻塞渲染 -->
<script src="https://www.googletagmanager.com/gtag/js"></script>
<script src="https://customer-service-sdk.com/widget.js"></script>

<!-- 优化后:异步加载,不阻塞 -->
<script async src="https://www.googletagmanager.com/gtag/js"></script>
<script defer src="https://customer-service-sdk.com/widget.js"></script>

对于非关键的第三方脚本,更好的做法是延迟到空闲时加载

js 复制代码
// 在浏览器空闲时才加载客服 SDK
if ('requestIdleCallback' in window) {
  requestIdleCallback(() => {
    const script = document.createElement('script');
    script.src = 'https://customer-service-sdk.com/widget.js';
    document.body.appendChild(script);
  });
} else {
  // 降级:延迟 3 秒
  setTimeout(() => {
    const script = document.createElement('script');
    script.src = 'https://customer-service-sdk.com/widget.js';
    document.body.appendChild(script);
  }, 3000);
}

3.3 Vite / Webpack 自动分块配置

js 复制代码
// vite.config.js --- Vite 6 的分块策略
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // React 生态单独打一个 chunk(稳定,缓存友好)
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],
          // UI 组件库单独打包
          'ui-vendor': ['antd', '@ant-design/icons'],
          // 工具库单独打包
          'utils-vendor': ['dayjs', 'lodash-es'],
        },
      },
    },
  },
});

3.4 效果验证

指标 优化前 优化后
vendor.js 大小 1.2MB 240KB ↓ 80%
TBT 820ms 340ms ↓ 58%
FCP 3.2s 1.9s ↓ 41%
Lighthouse 分 72 82 ↑ 10

四、优化三:CSS ------ 别让样式阻止首屏渲染

4.1 问题诊断

复制代码
❌ 全局 CSS 文件 180KB,包含所有未使用样式
❌ Noto Sans SC 字体 CSS 在 <head> 中同步加载
❌ Ant Design 全量样式被打包
❌ 首屏不需要的页面 CSS 也被加载

4.2 优化措施

Critical CSS 内联

首屏渲染需要的 CSS 直接内联到 HTML,其余异步加载:

js 复制代码
// vite.config.js --- 使用 vite-plugin-critical
import critical from 'vite-plugin-critical';

export default defineConfig({
  plugins: [
    critical({
      inline: true, // 内联
      dimensions: [
        { width: 375, height: 812 },  // 移动端优先
        { width: 1440, height: 900 }, // 桌面端
      ],
    }),
  ],
});

移除未使用的 CSS(PurgeCSS)

js 复制代码
// postcss.config.js
export default {
  plugins: [
    require('@fullhuman/postcss-purgecss')({
      content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
      safelist: ['html', 'body', '#root', /^ant-/],  // 保留 Ant Design 动态类名
    }),
  ],
};

按需引入组件库样式

js 复制代码
// vite.config.js --- antd 按需加载
import Components from 'unplugin-vue-components/vite'; // Vue
// 或 import Components from 'unplugin-antd'; // React

export default defineConfig({
  plugins: [
    Components({
      resolvers: [AntDesignResolver()],
    }),
  ],
});

4.3 效果验证

指标 优化前 优化后
CSS 总大小 180KB 45KB ↓ 75%
FCP 1.9s 1.2s ↓ 37%
Lighthouse 分 82 88 ↑ 6

五、优化四:字体 ------ 你的用户看到的可能是一片空白

5.1 问题诊断

css 复制代码
/* 问题1:FOIT --- 字体加载完成前文字不可见 */
@font-face {
  font-family: 'Noto Sans SC';
  src: url('/fonts/NotoSansSC-Regular.woff2');
  /* 缺少 font-display,浏览器默认行为是 FOIT */
}

/* 问题2:全量字体,Noto Sans SC 完整版约 6MB */

/* 问题3:字体文件没有预加载,等 CSSOM 构建完才开始下载 */

5.2 优化措施

css 复制代码
/* 优化后的 @font-face */
@font-face {
  font-family: 'Noto Sans SC';
  src: url('/fonts/NotoSansSC-Regular-subset.woff2') format('woff2');
  font-display: swap;         /* FOFT 策略:先用系统字体,加载后切换 */
  font-weight: 400;
  unicode-range: U+4E00-9FFF, U+3000-303F, U+FF00-FFEF; /* 只加载中文 */
}

font-display: swap 是性价比最高的 Web 字体策略。FOIT(不可见文本闪烁)比 FOUT(无样式文本闪烁)对用户体验的伤害大得多------用户对着白屏等 3 秒和看着字变一下,心理感受完全不同。

字体子集化

不要给用户加载整个字体文件:

bash 复制代码
# 使用 glyphhanger 工具生成子集
npx glyphhanger ./dist/index.html --subset=*.woff2

# 或用 Google Fonts 的 subset 参数
# https://fonts.googleapis.com/css2?family=Noto+Sans+SC&text=你的页面所有文字

预加载关键字体

html 复制代码
<head>
  <!-- 预加载:让字体文件在 CSS 解析前就开始下载 -->
  <link
    rel="preload"
    href="/fonts/NotoSansSC-Regular-subset.woff2"
    as="font"
    type="font/woff2"
    crossorigin
  />
</head>

5.3 效果验证

指标 优化前 优化后
字体文件大小 6MB 46KB ↓ 99%
文字可见时间 3.1s 0.6s ↓ 80%
CLS(字体导致) 0.15 0.02 ↓ 87%

六、优化五:CLS ------ 消灭布局抖动

6.1 CLS 是什么,为什么是 60→95 的决胜点

很多项目把 LCP 和 TBT 优化完就卡在 88-90 分,差的那几分几乎全是 CLS。

CLS = 元素在视口中的位移 × 位移距离占视口的比例。换句话说:页面加载过程中,任何你看到的元素位置发生意外移动,都在扣 CLS 分

6.2 最常见 CLS 元凶及修复

元凶一:图片没有宽高

html 复制代码
<!-- ❌ 图片加载完成后撑开布局,下方内容被挤下去 -->
<img src="banner.jpg" alt="Banner" />

<!-- ✅ 显式设置宽高或 aspect-ratio -->
<img 
  src="banner.jpg" 
  alt="Banner" 
  width="1200" 
  height="600" 
  style="max-width: 100%; height: auto;"
/>

元凶二:动态注入的内容顶开布局

css 复制代码
/* ❌ Cookie 弹窗/通知栏插入时,整个页面下移 */
/* ✅ 预留固定空间 */
.announcement-bar {
  min-height: 48px;  /* 即使内容为空也保留高度 */
}

.page-wrapper {
  /* 不要让 cookie banner 的插入改变文档流 */
  /* 使用 fixed 定位或 overlay 方式 */
}

元凶三:Web 字体导致的文字跳动

css 复制代码
/* ✅ 给 fallback 字体匹配相近的 size-adjust */
@font-face {
  font-family: 'Custom Font';
  src: url('/fonts/custom.woff2');
  font-display: swap;
  size-adjust: 105%;  /* 系统字体和自定义字体的尺寸差异补偿 */
}

元凶四:广告/嵌入式 iframe

css 复制代码
/* ✅ 为广告预留固定空间 */
.ad-slot {
  min-width: 300px;
  min-height: 250px;
  background: #f5f5f5;
}

.ad-slot:empty::after {
  content: 'Advertisement';
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: #999;
  font-size: 12px;
}

6.3 效果验证

指标 优化前 优化后
CLS 0.35 0.06 ↓ 83%
Lighthouse 分 88 95 ↑ 7

七、优化全景对比

复制代码
优化路线图:  60 → 72 → 82 → 88 → 95

┌──────────────────────────────────────────────────┐
│  步骤          问题              分数提升         │
├──────────────────────────────────────────────────┤
│  ① 图片优化    5.5MB→1.1MB       60 → 72 (+12)   │
│  ② JS优化      1.2MB→240KB       72 → 82 (+10)   │
│  ③ CSS优化     180KB→45KB        82 → 88 (+6)    │
│  ④ 字体优化    6MB→46KB          88 → 91 (+3)    │
│  ⑤ CLS消除     0.35→0.06         91 → 95 (+4)    │
└──────────────────────────────────────────────────┘

关键指标最终对比:

  FCP:  3.2s → 1.1s  ↓ 66%
  LCP:  5.8s → 2.1s  ↓ 64%
  TBT:  820ms → 150ms ↓ 82%
  CLS:  0.35 → 0.06   ↓ 83%
  SI:   6.1s → 2.8s   ↓ 54%

八、把优化结果锁死:接入 Lighthouse CI

辛苦优化完,最怕的就是下次发版被同事一个不经意的改动毁掉。用 Lighthouse CI 建立自动化护栏:

yaml 复制代码
# .github/workflows/lighthouse.yml
name: Lighthouse Performance Check
on: [pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci && npm run build

      - name: Run Lighthouse
        uses: treosh/lighthouse-ci-action@v12
        with:
          urls: |
            http://localhost:4173/
            http://localhost:4173/products
          budgetPath: .github/lighthouse/budget.json
          uploadArtifacts: true

      - name: Check thresholds
        run: |
          npx @lhci/cli autorun --config=.github/lighthouse/lighthouserc.js

性能预算配置(budget.json) --- 任何超过预算的 PR 直接拦截:

json 复制代码
{
  "performance": 90,
  "accessibility": 90,
  "best-practices": 90,
  "seo": 85,
  "resource-summary": [
    { "resourceType": "script", "budget": 300 },
    { "resourceType": "stylesheet", "budget": 50 },
    { "resourceType": "image", "budget": 500 },
    { "resourceType": "font", "budget": 80 }
  ]
}

本篇总结 & 下篇预告

本篇核心要点

  1. 先量化,后动手 --- 不看 Lighthouse 就做优化 = 盲目健身不称体重
  2. 图片永远是第一刀 --- 占 60%+ 体积,砍得最快
  3. JS 按路由切割、按需引入 --- moment.js → dayjs 这种替换 ROI 极高
  4. CSS 只给首屏需要的 --- Critical CSS 内联 + PurgeCSS
  5. 字体子集化 + font-display: swap --- 6MB 变 46KB
  6. CLS 是 90→95 的决胜点 --- 图片宽高、动态内容、字体 size-adjust
  7. 用 Lighthouse CI 锁死你的优化成果

🔜 下篇预告

第 2 篇:《资源加载策略全解》

  • HTTP/2 多路复用 vs HTTP/3 QUIC
  • 预加载 / 预连接 / 预获取的最佳实践
  • CDN 缓存策略和边缘计算(Edge Functions)
  • Service Worker 缓存策略
  • 资源优先级(fetchpriority)实战
相关推荐
思码梁田1 小时前
CSS letter-spacing 属性详解:掌控字符之间的呼吸感
前端·css
大尚来也1 小时前
React Hooks全解析:告别繁琐Class组件
前端·react.js·前端框架
不简说1 小时前
JS 代码技巧 vol.2 — 20 个数组/对象/字符串/正则的实用技巧
前端·javascript·面试
小小编程路2 小时前
C++ 块作用域详解:从基础到实战
前端·javascript·vue.js
虚惊一场2 小时前
MarkView:纯前端 Markdown 实时预览工具的架构设计
前端·javascript·vue.js
hunterandroid2 小时前
[鸿蒙从零到一] ArkUI 常用布局实战:Column、Row、Stack 与 Flex
前端
海上彼尚2 小时前
Nodejs也能写Agent - 17.LangGraph篇 - Checkpointer 与 Memory
前端·javascript·人工智能·langchain·node.js
盛夏绽放2 小时前
Hermes Agent 全栈安装、配置与Web端架构深度解析(官方命令版·最终优化)
前端·架构·hermes
早期的虫儿有鸟吃2 小时前
Vue3
前端·javascript·vue.js