Chrome DevTools + Lighthouse + Performance API:前端性能调优三件套实操指南

前言

做前端开发,性能优化是绕不过去的话题。线上用户反馈页面卡顿,老板说首屏加载太慢,产品经理要求 LCP 控制在 2.5 秒以内------这些需求几乎每个前端工程师都会遇到。

但很多人在面对性能问题时,要么凭感觉猜测瓶颈,要么只会跑一下 Lighthouse 看看分数,缺乏一套系统的排查和监控流程。实际上,Chrome DevTools、Lighthouse、Performance API 这三个工具各有分工,组合使用才能形成「发现问题 - 量化问题 - 持续监控」的完整闭环。

本文会从实操角度出发,手把手讲解这三个工具的核心用法,并在最后给出一个将三者串联起来的实战工作流。文中所有代码示例均可直接运行,适合有一定基础的前端开发者参考。

lua 复制代码
+------------------+      +------------------+      +------------------+
|  Chrome DevTools |      |    Lighthouse    |      | Performance API  |
|  (定位问题)       | ---> |  (量化评分)       | ---> |  (生产监控)       |
|                  |      |                  |      |                  |
|  - Performance   |      |  - 跑分 + 建议    |      |  - 自定义指标     |
|  - Network       |      |  - CI 集成       |      |  - 上报告警       |
+------------------+      +------------------+      +------------------+
        |                          |                          |
        +------------- 性能优化闭环 ---------------------------+

一、Chrome DevTools Performance 面板

1.1 如何录制性能剖析

打开 Chrome DevTools(F12),切换到 Performance 面板。在录制前,建议做以下准备工作:

  1. 开启「无痕模式」,避免浏览器插件干扰
  2. 在 Performance 面板勾选「Screenshots」和「Web Vitals」
  3. 将 CPU 节流设置为 4x slowdown,模拟中端设备
  4. 将 Network 设置为 Fast 3G,模拟弱网环境

点击左上角的圆形录制按钮(或使用 Ctrl+E),然后在页面上执行你要分析的操作(比如页面加载、滚动、点击按钮等),操作完成后再次点击停止录制。

ini 复制代码
录制流程:

  [开启无痕模式] --> [打开 DevTools] --> [设置 CPU/网络节流]
        |
        v
  [点击录制] --> [执行页面操作] --> [停止录制]
        |
        v
  [分析火焰图 / 长任务 / 帧率]

1.2 火焰图(Flame Chart)怎么看

录制完成后,你会看到一张色彩丰富的火焰图。理解火焰图是性能调优的基本功。

横轴:时间线,从左到右表示时间推移。

纵轴:调用栈深度,从上到下表示函数调用关系,上层函数调用下层函数。

颜色含义

颜色 含义 常见内容
黄色 JavaScript 执行 函数调用、事件处理、定时器回调
紫色 样式计算与布局(Layout) Recalculate Style、Layout
绿色 绘制(Paint) Paint、Composite Layers
灰色 系统级任务 Task、Run Microtasks
红色三角 长任务标记 超过 50ms 的任务会被标记

一个典型的页面加载火焰图结构大致如下:

lua 复制代码
时间轴 >>>

Main Thread:
|-- Parse HTML --|
                 |-- Evaluate Script (bundle.js) --|
                                                    |-- Run Microtasks --|
                                                    |-- Recalculate Style --|
                                                                            |-- Layout --|
                                                                                          |-- Paint --|

长任务(> 50ms)会在顶部用红色条纹标注

1.3 识别长任务(Long Tasks)

长任务是指主线程上执行时间超过 50ms 的任务。长任务会阻塞用户交互,导致页面卡顿。

在 Performance 面板中,长任务会在时间轴顶部用红色条纹标记。点击某个长任务,底部的 Summary 面板会显示该任务的时间分布:

yaml 复制代码
Summary 面板示例:

Total Time:        128.3 ms
Self Time:          12.1 ms

Breakdown:
  Scripting:        89.2 ms  (69.5%)   <-- JavaScript 执行占比最高
  Rendering:        24.7 ms  (19.3%)
  Painting:          8.1 ms   (6.3%)
  System:            6.3 ms   (4.9%)

实战技巧:如果 Scripting 占比最高,展开火焰图找到耗时最长的函数;如果 Rendering 占比高,很可能是触发了强制回流。

1.4 识别强制回流(Forced Reflow)

强制回流是性能杀手。当 JavaScript 在修改 DOM 样式后立即读取布局属性时,浏览器被迫同步执行布局计算。

在 Performance 面板中,强制回流会在火焰图中显示为紫色的「Layout」块,并带有红色的警告三角标记「Forced reflow is a likely performance bottleneck」。

以下是一个典型的强制回流代码及其修复:

typescript 复制代码
// 错误示范:循环中反复触发强制回流
function badLayout(elements: HTMLElement[]): void {
  for (const el of elements) {
    // 写操作
    el.style.width = '100px';
    // 紧跟读操作 -> 触发强制回流!
    const height = el.offsetHeight;
    el.style.height = `${height * 2}px`;
  }
}

// 正确做法:先批量读,再批量写(读写分离)
function goodLayout(elements: HTMLElement[]): void {
  // 阶段 1:批量读取
  const heights = elements.map((el) => el.offsetHeight);

  // 阶段 2:批量写入(不再触发强制回流)
  elements.forEach((el, i) => {
    el.style.width = '100px';
    el.style.height = `${heights[i] * 2}px`;
  });
}

也可以使用 requestAnimationFrame 将写操作推迟到下一帧:

typescript 复制代码
function safeLayout(el: HTMLElement): void {
  // 先读
  const currentHeight = el.offsetHeight;

  // 写操作推迟到下一帧
  requestAnimationFrame(() => {
    el.style.height = `${currentHeight + 10}px`;
  });
}

常见触发强制回流的属性

读取属性 说明
offsetTop/Left/Width/Height 元素的布局尺寸和位置
clientTop/Left/Width/Height 元素的可视区域尺寸
scrollTop/Left/Width/Height 元素的滚动位置和尺寸
getComputedStyle() 获取计算后的样式
getBoundingClientRect() 获取元素的边界矩形

二、Chrome DevTools Network 面板

2.1 瀑布流分析

Network 面板的瀑布流(Waterfall)是分析网络性能的利器。每个请求条的颜色和长度都有含义:

lua 复制代码
瀑布流各阶段:

|-- DNS Lookup --|-- Initial Connection --|-- SSL --|-- TTFB --|-- Content Download --|
   (深绿色)           (橙色)               (紫色)    (绿色)        (蓝色)

各阶段含义与优化方向:

阶段 含义 耗时长的优化方向
Queueing 请求排队等待 减少并发请求数、升级 HTTP/2
DNS Lookup DNS 解析 使用 dns-prefetch、减少域名数
Initial Connection TCP 连接建立 使用 preconnect、启用 keep-alive
SSL TLS 握手 升级 TLS 1.3、启用 Session Resumption
TTFB 首字节等待时间 优化服务端响应、使用 CDN
Content Download 内容下载 压缩资源、使用 Brotli/Gzip

2.2 瓶颈识别实战

打开 Network 面板后,按照以下步骤排查:

步骤一:按大小排序,找到体积最大的资源

点击 Size 列头进行排序,重点关注超过 200KB 的资源。常见问题包括:

  • 未压缩的 JavaScript 包
  • 未优化的图片
  • 冗余的第三方库

步骤二:按时间排序,找到耗时最长的请求

点击 Time 列头排序,关注 TTFB 过长的请求(通常超过 600ms 就需要关注)。

步骤三:使用筛选功能聚焦特定资源类型

lua 复制代码
常用过滤器:

  larger-than:100k         -- 大于 100KB 的资源
  -domain:cdn.example.com  -- 排除 CDN 域名
  status-code:304          -- 查看命中缓存的请求
  is:from-cache            -- 查看所有从缓存加载的请求
  mime-type:image           -- 只看图片资源

步骤四:检查请求优先级

在 Waterfall 列右键选择「Priority」列,查看浏览器为每个请求分配的优先级。如果关键资源(如首屏 CSS、主 JS 包)优先级偏低,可以使用 fetchpriority 属性提升:

html 复制代码
<!-- 提升关键图片的加载优先级 -->
<img src="hero.webp" fetchpriority="high" alt="Hero Image" />

<!-- 降低非关键脚本的加载优先级 -->
<link rel="preload" href="analytics.js" as="script" fetchpriority="low" />

2.3 利用 Coverage 面板检查代码覆盖率

按 Ctrl+Shift+P 打开命令面板,搜索「Show Coverage」。点击录制后刷新页面,Coverage 面板会显示每个 JS/CSS 文件的使用比例。

erlang 复制代码
Coverage 面板示例输出:

URL                          Type    Total Bytes   Unused Bytes   Usage
vendor.chunk.js              JS      342 KB        198 KB         42.1%
main.chunk.js                JS      156 KB         67 KB         57.1%
styles.css                   CSS      89 KB         52 KB         41.6%

红色条纹 = 未使用的代码    绿色条纹 = 已执行的代码

如果某个文件的未使用比例超过 50%,说明存在大量冗余代码,应该考虑代码分割或 tree-shaking。


三、Lighthouse

3.1 如何运行 Lighthouse

Lighthouse 有三种运行方式:

方式一:DevTools 内置面板

打开 DevTools -> Lighthouse 面板 -> 选择分析类别(Performance、Accessibility 等)-> 点击「Analyze page load」。

方式二:命令行运行(推荐用于 CI)

bash 复制代码
# 安装
npm install -g lighthouse

# 基本运行
lighthouse https://example.com --output html --output-path report.html

# 移动端模拟
lighthouse https://example.com --preset perf --form-factor mobile

# 输出 JSON 格式(便于程序化分析)
lighthouse https://example.com --output json --output-path report.json

# 多次运行取中位数(更准确)
lighthouse https://example.com --output json -n 3

方式三:Node.js API(CI 集成)

typescript 复制代码
import lighthouse from 'lighthouse';
import * as chromeLauncher from 'chrome-launcher';

interface LighthouseCategory {
  score: number;
}

interface LighthouseResult {
  categories: {
    performance: LighthouseCategory;
    accessibility: LighthouseCategory;
    'best-practices': LighthouseCategory;
    seo: LighthouseCategory;
  };
  audits: Record<string, { numericValue?: number }>;
}

async function runLighthouse(url: string): Promise<LighthouseResult> {
  const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });

  const options = {
    logLevel: 'info' as const,
    output: 'json' as const,
    port: chrome.port,
    onlyCategories: ['performance'],
    formFactor: 'mobile' as const,
    screenEmulation: {
      mobile: true,
      width: 375,
      height: 812,
      deviceScaleFactor: 3,
    },
    throttling: {
      cpuSlowdownMultiplier: 4,
      downloadThroughputKbps: 1600,
      uploadThroughputKbps: 750,
      rttMs: 150,
    },
  };

  const result = await lighthouse(url, options);
  await chrome.kill();

  const lhr = result?.lhr as LighthouseResult;
  return lhr;
}

// 使用示例
async function main(): Promise<void> {
  const result = await runLighthouse('https://example.com');

  console.log('Performance Score:', result.categories.performance.score * 100);
  console.log('FCP:', result.audits['first-contentful-paint']?.numericValue, 'ms');
  console.log('LCP:', result.audits['largest-contentful-paint']?.numericValue, 'ms');
  console.log('TBT:', result.audits['total-blocking-time']?.numericValue, 'ms');
  console.log('CLS:', result.audits['cumulative-layout-shift']?.numericValue);
}

main().catch(console.error);

3.2 Lighthouse 评分解读

Lighthouse Performance 分数由以下指标加权计算:

java 复制代码
Performance Score 构成 (Lighthouse 12):

  +-----------------------------------+--------+
  | 指标                              | 权重    |
  +-----------------------------------+--------+
  | First Contentful Paint (FCP)      |  10%   |
  | Largest Contentful Paint (LCP)    |  25%   |
  | Total Blocking Time (TBT)        |  30%   |
  | Cumulative Layout Shift (CLS)    |  25%   |
  | Speed Index (SI)                 |  10%   |
  +-----------------------------------+--------+

  评分区间:
  0-49   红色(差)
  50-89  橙色(需要改进)
  90-100 绿色(好)

各指标的目标值:

指标 好(绿色) 需改进(橙色) 差(红色)
FCP <= 1.8s 1.8s - 3.0s > 3.0s
LCP <= 2.5s 2.5s - 4.0s > 4.0s
TBT <= 200ms 200ms - 600ms > 600ms
CLS <= 0.1 0.1 - 0.25 > 0.25
SI <= 3.4s 3.4s - 5.8s > 5.8s

3.3 常见扣分项与修复方案

以下是 Lighthouse 报告中最常见的扣分项以及对应的修复方案:

3.3.1 Reduce unused JavaScript

问题:打包产物中包含大量未使用的代码。

修复方案

typescript 复制代码
// 错误:一次性导入整个库
import _ from 'lodash';
const result = _.get(obj, 'a.b.c');

// 正确:按需导入
import get from 'lodash/get';
const result = get(obj, 'a.b.c');

// 更好:路由级别的代码分割(React 示例)
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

3.3.2 Largest Contentful Paint element

问题:LCP 元素(通常是首屏大图或大段文字)加载太慢。

修复方案

html 复制代码
<!-- 1. 对 LCP 图片使用 preload -->
<link rel="preload" as="image" href="hero.webp" fetchpriority="high" />

<!-- 2. 使用现代图片格式 + 响应式图片 -->
<picture>
  <source srcset="hero.avif" type="image/avif" />
  <source srcset="hero.webp" type="image/webp" />
  <img
    src="hero.jpg"
    alt="Hero"
    width="1200"
    height="600"
    fetchpriority="high"
    decoding="async"
  />
</picture>

<!-- 3. 确保 LCP 图片不被 lazy loading -->
<!-- 错误:首屏图片不应该 lazy load -->
<img src="hero.jpg" loading="lazy" />
<!-- 正确:首屏图片使用 eager(默认值) -->
<img src="hero.jpg" loading="eager" fetchpriority="high" />

3.3.3 Eliminate render-blocking resources

问题:CSS 和 JS 文件阻塞了首屏渲染。

修复方案

html 复制代码
<!-- 关键 CSS 内联到 head 中 -->
<head>
  <style>
    /* 只放首屏关键样式 */
    .hero { display: flex; align-items: center; min-height: 100vh; }
    .nav { position: fixed; top: 0; width: 100%; }
  </style>

  <!-- 非关键 CSS 异步加载 -->
  <link
    rel="preload"
    href="non-critical.css"
    as="style"
    onload="this.onload=null;this.rel='stylesheet'"
  />
  <noscript>
    <link rel="stylesheet" href="non-critical.css" />
  </noscript>
</head>

<!-- 非关键 JS 使用 defer 或 async -->
<script src="main.js" defer></script>
<script src="analytics.js" async></script>

3.3.4 Cumulative Layout Shift 过高

问题:页面元素在加载过程中发生位移。

修复方案

css 复制代码
/* 1. 为图片和视频预留空间 */
img, video {
  aspect-ratio: 16 / 9;   /* 或者明确设置 width 和 height */
  width: 100%;
  height: auto;
}

/* 2. 为动态插入的内容预留空间 */
.ad-slot {
  min-height: 250px;       /* 预留广告位高度 */
}

.skeleton {
  min-height: 200px;       /* 骨架屏预留高度 */
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
typescript 复制代码
// 3. 避免在已有内容上方动态插入元素
// 错误:在页面顶部插入通知横幅导致内容下移
function showBanner(): void {
  const banner = document.createElement('div');
  banner.textContent = 'New feature available!';
  document.body.prepend(banner);  // 会导致所有内容下移 -> CLS
}

// 正确:使用固定定位或预留空间
function showBannerSafe(): void {
  const placeholder = document.getElementById('banner-slot');
  if (placeholder) {
    placeholder.textContent = 'New feature available!';
    placeholder.style.display = 'block';
    // placeholder 已经在 DOM 中预留了空间,不会导致 CLS
  }
}

3.4 在 CI 中集成 Lighthouse

将 Lighthouse 集成到 CI 流程中,可以在每次提交时自动检测性能退化:

typescript 复制代码
// lighthouse-ci.config.ts
interface LhciConfig {
  ci: {
    collect: {
      url: string[];
      numberOfRuns: number;
      startServerCommand: string;
    };
    assert: {
      assertions: Record<string, [string, { minScore: number }]>;
    };
    upload: {
      target: string;
      serverBaseUrl?: string;
    };
  };
}

const config: LhciConfig = {
  ci: {
    collect: {
      url: ['http://localhost:3000/', 'http://localhost:3000/dashboard'],
      numberOfRuns: 3,
      startServerCommand: 'npm run start',
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'first-contentful-paint': ['warn', { minScore: 0 }],
        'largest-contentful-paint': ['error', { minScore: 0 }],
        'cumulative-layout-shift': ['error', { minScore: 0 }],
        'total-blocking-time': ['error', { minScore: 0 }],
      },
    },
    upload: {
      target: 'lhci',
      serverBaseUrl: 'https://lhci.your-domain.com',
    },
  },
};

export default config;

配合 GitHub Actions 使用:

yaml 复制代码
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push, 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
      - run: npm run build

      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v12
        with:
          configPath: './lighthouse-ci.config.ts'
          uploadArtifacts: true
          temporaryPublicStorage: true

四、Performance API

Performance API 是浏览器提供的原生性能测量接口,可以在生产环境中采集真实用户的性能数据(RUM,Real User Monitoring)。

Navigation Timing API 提供了页面导航过程中每个阶段的精确时间戳。

lua 复制代码
Navigation Timing 时间线:

navigationStart
    |-- redirectStart/End --|
                            |-- fetchStart --|
                                             |-- domainLookupStart/End --|
                                                                        |-- connectStart/End --|
                                                                                               |-- requestStart --|
                                                                                                                  |-- responseStart/End --|
                                                                                                                                          |-- domLoading --|
                                                                                                                                                           |-- domInteractive --|
                                                                                                                                                                                |-- domContentLoaded --|
                                                                                                                                                                                                       |-- domComplete --|
                                                                                                                                                                                                                         |-- loadEventEnd

获取各阶段耗时的代码:

typescript 复制代码
interface NavigationMetrics {
  redirect: number;
  dns: number;
  tcp: number;
  ssl: number;
  ttfb: number;
  download: number;
  domParse: number;
  domContentLoaded: number;
  domComplete: number;
  fullLoad: number;
}

function getNavigationMetrics(): NavigationMetrics | null {
  const entries = performance.getEntriesByType('navigation');
  if (entries.length === 0) return null;

  const nav = entries[0] as PerformanceNavigationTiming;

  return {
    // 重定向耗时
    redirect: nav.redirectEnd - nav.redirectStart,
    // DNS 解析耗时
    dns: nav.domainLookupEnd - nav.domainLookupStart,
    // TCP 连接耗时
    tcp: nav.connectEnd - nav.connectStart,
    // SSL 握手耗时(HTTPS 页面)
    ssl: nav.secureConnectionStart > 0
      ? nav.connectEnd - nav.secureConnectionStart
      : 0,
    // TTFB(首字节时间)
    ttfb: nav.responseStart - nav.requestStart,
    // 内容下载耗时
    download: nav.responseEnd - nav.responseStart,
    // DOM 解析耗时
    domParse: nav.domInteractive - nav.responseEnd,
    // DOMContentLoaded 事件耗时
    domContentLoaded: nav.domContentLoadedEventEnd - nav.domContentLoadedEventStart,
    // DOM 完成耗时
    domComplete: nav.domComplete - nav.domInteractive,
    // 完整页面加载时间
    fullLoad: nav.loadEventEnd - nav.startTime,
  };
}

// 使用:在页面加载完成后采集
window.addEventListener('load', () => {
  // 延迟执行,确保 loadEventEnd 已经有值
  setTimeout(() => {
    const metrics = getNavigationMetrics();
    if (metrics) {
      console.table(metrics);
      // 上报到你的监控系统
      reportToServer('/api/metrics/navigation', metrics);
    }
  }, 0);
});

4.2 Resource Timing:资源加载分析

Resource Timing API 可以采集页面中每个资源的详细加载时间。

typescript 复制代码
interface ResourceMetric {
  name: string;
  type: string;
  duration: number;
  size: number;
  protocol: string;
  dns: number;
  tcp: number;
  ttfb: number;
  download: number;
  cached: boolean;
}

function getSlowResources(threshold: number = 1000): ResourceMetric[] {
  const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[];

  return entries
    .filter((entry) => entry.duration > threshold)
    .map((entry) => ({
      name: entry.name.split('/').pop() || entry.name,
      type: entry.initiatorType,
      duration: Math.round(entry.duration),
      size: entry.transferSize,
      protocol: entry.nextHopProtocol,
      dns: Math.round(entry.domainLookupEnd - entry.domainLookupStart),
      tcp: Math.round(entry.connectEnd - entry.connectStart),
      ttfb: Math.round(entry.responseStart - entry.requestStart),
      download: Math.round(entry.responseEnd - entry.responseStart),
      cached: entry.transferSize === 0,
    }))
    .sort((a, b) => b.duration - a.duration);
}

// 分析资源加载汇总信息
function getResourceSummary(): void {
  const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[];

  const summary: Record<string, { count: number; totalSize: number; totalDuration: number }> = {};

  for (const entry of entries) {
    const type = entry.initiatorType;
    if (!summary[type]) {
      summary[type] = { count: 0, totalSize: 0, totalDuration: 0 };
    }
    summary[type].count++;
    summary[type].totalSize += entry.transferSize;
    summary[type].totalDuration += entry.duration;
  }

  console.table(summary);
}

// 使用
window.addEventListener('load', () => {
  setTimeout(() => {
    console.log('--- Slow Resources (> 1000ms) ---');
    console.table(getSlowResources(1000));

    console.log('--- Resource Summary ---');
    getResourceSummary();
  }, 100);
});

4.3 PerformanceObserver:实时监听性能事件

PerformanceObserver 是现代 Performance API 的核心,它可以异步监听各类性能事件,而不需要轮询。

typescript 复制代码
// 监听 LCP(Largest Contentful Paint)
function observeLCP(callback: (value: number, element: string) => void): void {
  const observer = new PerformanceObserver((list) => {
    const entries = list.getEntries();
    // LCP 可能会多次触发,取最后一个值
    const lastEntry = entries[entries.length - 1] as PerformanceEntry & {
      element?: Element;
    };

    callback(
      lastEntry.startTime,
      lastEntry.element?.tagName || 'unknown'
    );
  });

  observer.observe({ type: 'largest-contentful-paint', buffered: true });
}

// 监听 FID(First Input Delay)
function observeFID(callback: (value: number, eventType: string) => void): void {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      const fidEntry = entry as PerformanceEntry & {
        processingStart: number;
        name: string;
      };
      const delay = fidEntry.processingStart - fidEntry.startTime;
      callback(delay, fidEntry.name);
    }
  });

  observer.observe({ type: 'first-input', buffered: true });
}

// 监听 CLS(Cumulative Layout Shift)
function observeCLS(callback: (value: number) => void): void {
  let clsValue = 0;
  let sessionValue = 0;
  let sessionEntries: PerformanceEntry[] = [];

  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      const layoutShift = entry as PerformanceEntry & {
        hadRecentInput: boolean;
        value: number;
      };

      // 排除用户输入引起的布局偏移
      if (layoutShift.hadRecentInput) continue;

      const firstSessionEntry = sessionEntries[0];
      const lastSessionEntry = sessionEntries[sessionEntries.length - 1];

      // 如果与上一个条目间隔小于 1 秒,且与会话起始间隔小于 5 秒,
      // 则归入当前会话窗口
      if (
        lastSessionEntry &&
        entry.startTime - lastSessionEntry.startTime < 1000 &&
        firstSessionEntry &&
        entry.startTime - firstSessionEntry.startTime < 5000
      ) {
        sessionValue += layoutShift.value;
      } else {
        sessionValue = layoutShift.value;
        sessionEntries = [];
      }

      sessionEntries.push(entry);

      if (sessionValue > clsValue) {
        clsValue = sessionValue;
        callback(clsValue);
      }
    }
  });

  observer.observe({ type: 'layout-shift', buffered: true });
}

// 监听长任务
function observeLongTasks(callback: (duration: number, attribution: string) => void): void {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      const longTask = entry as PerformanceEntry & {
        attribution?: Array<{ containerType?: string; containerName?: string }>;
      };

      const attr = longTask.attribution?.[0];
      const source = attr
        ? `${attr.containerType || 'unknown'}:${attr.containerName || 'unknown'}`
        : 'unknown';

      callback(entry.duration, source);
    }
  });

  observer.observe({ type: 'longtask', buffered: true });
}

4.4 自定义性能指标:User Timing API

除了浏览器自动采集的指标,我们还可以用 User Timing API 测量业务逻辑的耗时:

typescript 复制代码
// 基础用法:mark + measure
function measureDataFetch(): void {
  // 标记开始点
  performance.mark('data-fetch-start');

  fetch('/api/dashboard')
    .then((res) => res.json())
    .then((data) => {
      // 标记结束点
      performance.mark('data-fetch-end');

      // 创建测量
      performance.measure('data-fetch', 'data-fetch-start', 'data-fetch-end');

      // 读取测量结果
      const measures = performance.getEntriesByName('data-fetch', 'measure');
      console.log(`Data fetch took ${measures[0].duration.toFixed(2)}ms`);

      // 清理
      performance.clearMarks('data-fetch-start');
      performance.clearMarks('data-fetch-end');
      performance.clearMeasures('data-fetch');
    });
}

// 封装一个通用的性能测量工具
class PerfTracker {
  private static instance: PerfTracker;
  private observer: PerformanceObserver;
  private metrics: Map<string, number[]> = new Map();

  private constructor() {
    this.observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'measure') {
          const values = this.metrics.get(entry.name) || [];
          values.push(entry.duration);
          this.metrics.set(entry.name, values);
        }
      }
    });
    this.observer.observe({ type: 'measure', buffered: true });
  }

  static getInstance(): PerfTracker {
    if (!PerfTracker.instance) {
      PerfTracker.instance = new PerfTracker();
    }
    return PerfTracker.instance;
  }

  // 测量同步函数
  measureSync<T>(name: string, fn: () => T): T {
    performance.mark(`${name}-start`);
    const result = fn();
    performance.mark(`${name}-end`);
    performance.measure(name, `${name}-start`, `${name}-end`);
    return result;
  }

  // 测量异步函数
  async measureAsync<T>(name: string, fn: () => Promise<T>): Promise<T> {
    performance.mark(`${name}-start`);
    try {
      const result = await fn();
      return result;
    } finally {
      performance.mark(`${name}-end`);
      performance.measure(name, `${name}-start`, `${name}-end`);
    }
  }

  // 获取统计数据
  getStats(name: string): { avg: number; p50: number; p95: number; count: number } | null {
    const values = this.metrics.get(name);
    if (!values || values.length === 0) return null;

    const sorted = [...values].sort((a, b) => a - b);
    const sum = sorted.reduce((a, b) => a + b, 0);

    return {
      avg: Math.round(sum / sorted.length),
      p50: Math.round(sorted[Math.floor(sorted.length * 0.5)]),
      p95: Math.round(sorted[Math.floor(sorted.length * 0.95)]),
      count: sorted.length,
    };
  }

  // 获取所有指标的汇总
  getAllStats(): Record<string, { avg: number; p50: number; p95: number; count: number }> {
    const result: Record<string, { avg: number; p50: number; p95: number; count: number }> = {};
    for (const name of this.metrics.keys()) {
      const stats = this.getStats(name);
      if (stats) result[name] = stats;
    }
    return result;
  }
}

// 使用示例
const tracker = PerfTracker.getInstance();

// 测量组件渲染时间
tracker.measureSync('render-user-list', () => {
  renderUserList(users);
});

// 测量 API 调用时间
await tracker.measureAsync('fetch-orders', async () => {
  const res = await fetch('/api/orders');
  return res.json();
});

// 查看统计数据
console.table(tracker.getAllStats());

4.5 性能数据上报

采集到的性能数据需要上报到后端,用于分析和告警。上报时需要注意不影响页面性能:

typescript 复制代码
interface PerformancePayload {
  url: string;
  timestamp: number;
  userAgent: string;
  connection?: string;
  metrics: Record<string, number>;
}

class PerformanceReporter {
  private buffer: PerformancePayload[] = [];
  private readonly endpoint: string;
  private readonly batchSize: number;
  private readonly flushInterval: number;
  private timer: ReturnType<typeof setInterval> | null = null;

  constructor(
    endpoint: string,
    options: { batchSize?: number; flushInterval?: number } = {}
  ) {
    this.endpoint = endpoint;
    this.batchSize = options.batchSize || 10;
    this.flushInterval = options.flushInterval || 5000;

    this.startAutoFlush();
    this.setupPageHideFlush();
  }

  add(metrics: Record<string, number>): void {
    const nav = navigator as Navigator & {
      connection?: { effectiveType?: string };
    };

    const payload: PerformancePayload = {
      url: location.href,
      timestamp: Date.now(),
      userAgent: navigator.userAgent,
      connection: nav.connection?.effectiveType,
      metrics,
    };

    this.buffer.push(payload);

    if (this.buffer.length >= this.batchSize) {
      this.flush();
    }
  }

  private flush(): void {
    if (this.buffer.length === 0) return;

    const data = [...this.buffer];
    this.buffer = [];

    // 使用 sendBeacon 上报,不阻塞页面卸载
    const blob = new Blob([JSON.stringify(data)], {
      type: 'application/json',
    });
    const sent = navigator.sendBeacon(this.endpoint, blob);

    // sendBeacon 失败时降级为 fetch
    if (!sent) {
      fetch(this.endpoint, {
        method: 'POST',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' },
        keepalive: true,
      }).catch(() => {
        // 静默失败,不影响用户体验
      });
    }
  }

  private startAutoFlush(): void {
    this.timer = setInterval(() => this.flush(), this.flushInterval);
  }

  private setupPageHideFlush(): void {
    // 页面隐藏或卸载时立即上报
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        this.flush();
      }
    });
  }

  destroy(): void {
    if (this.timer) {
      clearInterval(this.timer);
    }
    this.flush();
  }
}

// 使用示例
const reporter = new PerformanceReporter('/api/performance', {
  batchSize: 5,
  flushInterval: 10000,
});

// 采集 Web Vitals 并上报
observeLCP((value) => {
  reporter.add({ lcp: Math.round(value) });
});

observeFID((value) => {
  reporter.add({ fid: Math.round(value) });
});

observeCLS((value) => {
  reporter.add({ cls: value });
});

observeLongTasks((duration) => {
  reporter.add({ longTaskDuration: Math.round(duration) });
});

五、实战工作流:三件套组合使用

前面分别介绍了每个工具的用法,下面用一个具体场景演示如何将它们串联起来形成完整的优化闭环。

场景描述

某电商网站首页加载缓慢,用户反馈从点击到看到商品列表需要等待较长时间。需要定位问题、优化并建立持续监控。

第一步:DevTools 定位问题

markdown 复制代码
排查流程:

  1. 打开无痕模式 + DevTools
  2. 切换到 Performance 面板,设置 4x CPU + Fast 3G
  3. 录制页面加载过程
  4. 分析结果

发现的问题:
  [问题 A] 火焰图显示 main.js 执行耗时 1200ms(长任务)
            |-- 展开发现是 JSON.parse 处理一个 2MB 的商品数据
  
  [问题 B] Network 面板发现 hero-image.png 体积 3.2MB,
            TTFB 正常但 Content Download 耗时 4.1s
  
  [问题 C] Performance 面板出现紫色 Layout 块(Forced Reflow),
            定位到商品卡片的渲染函数

第二步:Lighthouse 量化问题

运行 Lighthouse 得到基线分数:

yaml 复制代码
Lighthouse 基线报告:

  Performance Score:  42
  ---------------------------------
  FCP:   2.8s   (橙色)
  LCP:   6.2s   (红色)     <-- 主要由 hero 大图导致
  TBT:   890ms  (红色)     <-- main.js 长任务导致
  CLS:   0.32   (红色)     <-- 商品卡片渲染导致
  SI:    5.1s   (橙色)

  关键建议:
  - Properly size images           (预估节省 3.1s)
  - Reduce unused JavaScript       (预估节省 0.8s)
  - Avoid large layout shifts      (CLS 改善)

第三步:逐个修复

typescript 复制代码
// 修复问题 A:将大数据处理移到 Web Worker
// worker.ts
self.addEventListener('message', (e: MessageEvent<string>) => {
  const data = JSON.parse(e.data);
  // 在 Worker 中执行耗时的数据处理
  const processed = data.products.map((p: { id: string; name: string; price: number }) => ({
    id: p.id,
    name: p.name,
    price: p.price,
  }));
  self.postMessage(processed);
});

// main.ts
async function loadProducts(): Promise<void> {
  const response = await fetch('/api/products');
  const text = await response.text();

  const worker = new Worker(new URL('./worker.ts', import.meta.url));

  return new Promise((resolve) => {
    worker.onmessage = (e) => {
      renderProducts(e.data);
      worker.terminate();
      resolve();
    };
    worker.postMessage(text);
  });
}
typescript 复制代码
// 修复问题 B:图片优化
// next.config.js(Next.js 示例)或手动配置
// 1. 将 PNG 转为 WebP/AVIF
// 2. 使用响应式图片
// 3. 实现图片 CDN 处理

function getOptimizedImageUrl(
  src: string,
  width: number,
  format: 'webp' | 'avif' = 'webp'
): string {
  // 假设使用图片 CDN 服务
  return `https://cdn.example.com/resize?src=${encodeURIComponent(src)}&w=${width}&fmt=${format}&q=80`;
}

// 实现渐进式加载:先显示低质量占位图,再加载高清图
function createProgressiveImage(src: string, container: HTMLElement): void {
  // 先加载低质量的模糊占位图(约 1-2KB)
  const placeholder = document.createElement('img');
  placeholder.src = getOptimizedImageUrl(src, 40, 'webp');
  placeholder.style.filter = 'blur(20px)';
  placeholder.style.width = '100%';
  placeholder.style.aspectRatio = '16/9';
  container.appendChild(placeholder);

  // 异步加载高清图
  const fullImage = new Image();
  fullImage.src = getOptimizedImageUrl(src, 1200, 'webp');
  fullImage.onload = () => {
    placeholder.src = fullImage.src;
    placeholder.style.filter = 'none';
  };
}
typescript 复制代码
// 修复问题 C:消除强制回流
// 优化前:每张商品卡片渲染时都在读写交替
function renderProductCardBad(product: { name: string; image: string }): void {
  const card = document.createElement('div');
  card.className = 'product-card';
  card.innerHTML = `<img src="${product.image}" /><h3>${product.name}</h3>`;
  document.getElementById('product-list')!.appendChild(card);

  // 读取布局属性 -> 强制回流
  const cardHeight = card.offsetHeight;
  card.style.minHeight = `${cardHeight}px`;
}

// 优化后:使用 CSS 固定尺寸 + 批量 DOM 操作
function renderProductCards(products: { name: string; image: string }[]): void {
  // 使用 DocumentFragment 批量操作 DOM
  const fragment = document.createDocumentFragment();

  for (const product of products) {
    const card = document.createElement('div');
    card.className = 'product-card'; // CSS 中已定义固定高度
    card.innerHTML = `
      <img src="${product.image}" width="280" height="210" loading="lazy" />
      <h3>${product.name}</h3>
    `;
    fragment.appendChild(card);
  }

  // 一次性挂载到 DOM
  document.getElementById('product-list')!.appendChild(fragment);
}

第四步:用 Lighthouse 验证优化效果

yaml 复制代码
Lighthouse 优化后报告:

  Performance Score:  91  (+49)
  ---------------------------------
  FCP:   1.2s   (绿色)     +1.6s 提升
  LCP:   2.1s   (绿色)     +4.1s 提升
  TBT:   120ms  (绿色)     +770ms 提升
  CLS:   0.05   (绿色)     +0.27 提升
  SI:    2.8s   (绿色)     +2.3s 提升

第五步:Performance API 建立生产监控

typescript 复制代码
// performance-monitor.ts
// 完整的生产环境性能监控方案

interface WebVitals {
  lcp?: number;
  fid?: number;
  cls?: number;
  fcp?: number;
  ttfb?: number;
  inp?: number;
}

function initPerformanceMonitor(reportEndpoint: string): void {
  const vitals: WebVitals = {};
  const reporter = new PerformanceReporter(reportEndpoint);

  // 1. 采集 LCP
  observeLCP((value) => {
    vitals.lcp = Math.round(value);
  });

  // 2. 采集 FID
  observeFID((value) => {
    vitals.fid = Math.round(value);
  });

  // 3. 采集 CLS
  observeCLS((value) => {
    vitals.cls = parseFloat(value.toFixed(4));
  });

  // 4. 采集 FCP
  const fcpObserver = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.name === 'first-contentful-paint') {
        vitals.fcp = Math.round(entry.startTime);
      }
    }
  });
  fcpObserver.observe({ type: 'paint', buffered: true });

  // 5. 采集 TTFB
  const navEntries = performance.getEntriesByType('navigation');
  if (navEntries.length > 0) {
    const nav = navEntries[0] as PerformanceNavigationTiming;
    vitals.ttfb = Math.round(nav.responseStart - nav.requestStart);
  }

  // 6. 采集 INP(Interaction to Next Paint)
  let maxINP = 0;
  const inpObserver = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      const eventEntry = entry as PerformanceEntry & {
        processingStart: number;
        processingEnd: number;
      };
      const duration = entry.duration;
      if (duration > maxINP) {
        maxINP = duration;
        vitals.inp = Math.round(duration);
      }
    }
  });
  inpObserver.observe({ type: 'event', buffered: true });

  // 7. 在页面隐藏时上报所有指标
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
      reporter.add(vitals as Record<string, number>);
    }
  });

  // 8. 监控长任务并告警
  observeLongTasks((duration, attribution) => {
    if (duration > 200) {
      // 超过 200ms 的长任务触发告警
      reporter.add({
        longTaskDuration: Math.round(duration),
        longTaskSource: attribution as unknown as number,
      });
      console.warn(`[Performance] Long task detected: ${duration}ms from ${attribution}`);
    }
  });

  // 9. 监控慢接口
  const resourceObserver = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      const resource = entry as PerformanceResourceTiming;
      if (
        resource.initiatorType === 'fetch' ||
        resource.initiatorType === 'xmlhttprequest'
      ) {
        if (resource.duration > 3000) {
          reporter.add({
            slowApiUrl: resource.name as unknown as number,
            slowApiDuration: Math.round(resource.duration),
            slowApiTtfb: Math.round(resource.responseStart - resource.requestStart),
          });
        }
      }
    }
  });
  resourceObserver.observe({ type: 'resource', buffered: false });
}

// 应用入口调用
initPerformanceMonitor('/api/rum/collect');

三件套组合流程图

lua 复制代码
+================================================================+
|                    性能优化完整工作流                             |
+================================================================+

  开发阶段                    发布前                    生产环境
  --------                    ------                    --------

  DevTools Performance        Lighthouse CI             Performance API
  DevTools Network            性能门禁                  RUM 监控
       |                           |                         |
       v                           v                         v
  +------------+             +------------+            +------------+
  | 录制分析    |             | 自动跑分    |            | 实时采集    |
  | 火焰图      |             | 基准对比    |            | Web Vitals  |
  | 瀑布流      |             | PR 评论     |            | 自定义指标  |
  | 强制回流    |             | 阈值告警    |            | 上报存储    |
  +------------+             +------------+            +------------+
       |                           |                         |
       +------ 发现问题 -----------+------ 量化验证 ---------+
                                   |
                                   v
                          +----------------+
                          | 定位根因        |
                          | 实施优化        |
                          | 验证效果        |
                          | 持续监控        |
                          +----------------+

以下是三者的定位对比表:

维度 DevTools Lighthouse Performance API
使用阶段 开发 / 调试 开发 / CI 生产环境
数据类型 实验室数据 实验室数据 真实用户数据(RUM)
主要用途 定位具体性能瓶颈 量化评分 + 优化建议 持续监控 + 告警
粒度 函数级别 / 请求级别 页面级别 可自定义
自动化 手动操作 可 CI 集成 自动采集上报
优势 深入定位根因 标准化评分,易于沟通 反映真实用户体验
局限 只能代表本地环境 模拟环境可能与生产有差异 需要自建采集和分析系统

六、进阶技巧

6.1 使用 web-vitals 库简化采集

Google 维护的 web-vitals 库封装了 Web Vitals 的采集逻辑,比手写 PerformanceObserver 更健壮:

typescript 复制代码
import { onLCP, onFID, onCLS, onFCP, onTTFB, onINP } from 'web-vitals';
import type { Metric } from 'web-vitals';

function sendToAnalytics(metric: Metric): void {
  const body = {
    name: metric.name,
    value: metric.value,
    rating: metric.rating,       // 'good' | 'needs-improvement' | 'poor'
    delta: metric.delta,         // 自上次报告以来的变化量
    id: metric.id,               // 唯一标识(用于去重)
    navigationType: metric.navigationType,
    url: location.href,
  };

  // 使用 sendBeacon 上报
  navigator.sendBeacon('/api/vitals', JSON.stringify(body));
}

// 一行代码搞定所有 Web Vitals 采集
onLCP(sendToAnalytics);
onFID(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
onINP(sendToAnalytics);

6.2 DevTools 中的 Performance Insights 面板

Chrome 新推出的 Performance Insights 面板(Ctrl+Shift+I -> Performance Insights)提供了比传统 Performance 面板更易读的分析视图:

  • 自动标注 LCP、FCP、CLS 等关键指标
  • 以交互时间线的形式展示,而非原始火焰图
  • 自动关联 Insight 建议(如"此长任务阻塞了交互")

这个面板特别适合快速诊断,而传统 Performance 面板适合深入分析。

6.3 Chrome DevTools Protocol 自动化

对于需要批量分析的场景,可以通过 Chrome DevTools Protocol 实现自动化:

typescript 复制代码
import puppeteer from 'puppeteer';

interface TraceEvent {
  cat: string;
  name: string;
  dur?: number;
}

async function automatedPerfAudit(url: string): Promise<void> {
  const browser = await puppeteer.launch({
    headless: true,
    args: ['--no-sandbox'],
  });

  const page = await browser.newPage();

  // 模拟移动端
  await page.emulate(puppeteer.KnownDevices['iPhone 12']);

  // 开启 tracing(等同于 Performance 面板录制)
  await page.tracing.start({
    screenshots: true,
    categories: ['devtools.timeline'],
  });

  // 导航并等待加载完成
  await page.goto(url, { waitUntil: 'networkidle0' });

  // 停止 tracing
  const traceBuffer = await page.tracing.stop();
  const traceData = JSON.parse(traceBuffer.toString());

  // 分析长任务
  const longTasks = traceData.traceEvents
    .filter(
      (e: TraceEvent) =>
        e.cat === 'devtools.timeline' &&
        e.name === 'RunTask' &&
        e.dur &&
        e.dur > 50000 // 微秒,50ms = 50000us
    )
    .map((e: TraceEvent) => ({
      duration: (e.dur! / 1000).toFixed(1) + 'ms',
    }));

  console.log('Long Tasks:', longTasks);

  // 提取资源加载信息
  const resources = await page.evaluate(() => {
    return performance
      .getEntriesByType('resource')
      .map((r) => {
        const resource = r as PerformanceResourceTiming;
        return {
          name: resource.name.split('/').pop(),
          duration: Math.round(resource.duration),
          size: resource.transferSize,
        };
      })
      .sort((a, b) => b.duration - a.duration)
      .slice(0, 10);
  });

  console.log('Top 10 Slowest Resources:', resources);

  await browser.close();
}

automatedPerfAudit('https://example.com').catch(console.error);

总结

Chrome DevTools、Lighthouse、Performance API 三者各有侧重,分别解决性能优化链路中不同阶段的需求:

  1. DevTools 是「显微镜」,用于在开发阶段深入定位性能瓶颈,精确到函数调用和单个网络请求。掌握火焰图、长任务识别和瀑布流分析是基本功。

  2. Lighthouse 是「体检报告」,用标准化的评分体系量化页面性能,提供可操作的优化建议。集成到 CI 后可以充当性能门禁,防止性能退化。

  3. Performance API 是「监控探针」,部署到生产环境后持续采集真实用户的性能数据。实验室数据再好看,也不如 RUM 数据真实。

三者的最佳实践是组合使用:用 DevTools 发现和定位问题,用 Lighthouse 量化优化效果并设置门禁,用 Performance API 在生产环境中持续监控。性能优化不是一次性的工作,而是一个需要持续关注的过程。

如果觉得有帮助,欢迎点赞收藏关注,后续会继续分享前端性能优化相关的实践。

相关推荐
Appoint_x1 小时前
设计稿自己会说话:我用 Claude 给 Figma 做了个 AI 上下文插件
前端·javascript
豹哥学前端1 小时前
浏览器console里的双中括号 `[[ ]]`
前端·javascript·ecmascript 6
菜泡泡@1 小时前
npm 安装pnpm之后运行pnpm -v查询报错
前端·npm·node.js
贫民窟的勇敢爷们1 小时前
React跨平台能力,打破前端开发的平台边界
前端·react.js·前端框架
lifejump2 小时前
Dede(织梦)CMS渗透测试(all)
前端·网络·安全·web安全
扬帆破浪2 小时前
sidecar崩溃后前端怎么续命 重启策略与状态保留
前端·人工智能·架构·开源·知识图谱
光影少年2 小时前
前端算法题
前端·javascript·算法
Lee川2 小时前
从输入框到智能匹配:一文读懂搜索功能的完整实现
前端·后端