使用 Lighthouse 分析前端性能
lighthouse 是 Google Chrome 推出的一款开源自动化工具,它可以搜集多个现代网页性能指标,分析 Web 应用的性能并生成报告,为开发人员进行性能优化的提供了参考方向。
使用入门
在 Chrome DevTools 中使用
比较推荐的方法。我们可以直接打开调试面板中的 “Lighthouse”面板,然后点击生成报告:

使用 Chrome 插件
我们可以在应用商城下载并将插件添加到浏览器,点击插件面板开始分析,使用方法和上面类似。
在 Node 命令行工具中使用
在调试面板和插件中只能进行一些基本的配置,如果我们想要更加灵活地配置分析的内容,我们可以在 Node 命令行工具中使用:
- 安装
npm install -g lighthouse
# or use yarn:
# yarn global add lighthouse
- 分析:以
www.baidu.com
为例,使用以下命令可以生成一份中文报告:
lighthouse https://www.baidu.com/ --locale=zh-CN --preset=desktop --disable-network-throttling=true --disable-storage-reset=true

- 登录验证:有的时候,我们需要拿到权限才能访问页面,这个时候,直接执行以上的命令会报错或者分析到的不是目标页。这里,需要我们手动登录或者借助 Puppeteer 模拟登录。参考文档,我们可以这样处理:
- 运行
chrome-debug
启动一个 Chrome 实例(全局安装了 lighthouse 之后) - 访问我们的目标网址并完成登录
- 在新的终端,运行上面的命令
lighthouse [https://example.com](https://www.baidu.com/) --preset=desktop --port=chrome.port
- 更多的配置见 cli-options
使用 Node module
我们还可以在自己的本地项目中,将 Lighthouse 作为一个 module 使用:
- 安装
yarn add --dev lighthouse
- 自定义启动
const fs = require("fs");
const lighthouse = require("lighthouse");
const chromeLauncher = require("chrome-launcher");
const launchChromeAndRunLighthouse = async (url, opts, config = null) => {
const options = {
logLevel: "info",
output: "html",
onlyCategories: ["performance"],
...opts,
};
// 打开 chrome debug
const chrome = await chromeLauncher.launch({ chromeFlags: opts.chromeFlags });
// 开始分析
const runnerResult = await lighthouse(
url,
{ ...options, port: chrome.port },
config
);
await chrome.kill();
// 生成报告
const reportHtml = runnerResult.report;
fs.writeFileSync("lhreport.html", reportHtml);
};
// 启动
const run = async () => {
const opts = {
chromeFlags: ["--show-paint-rects"],
preset: "desktop",
};
await launchChromeAndRunLighthouse("https://example.com", opts);
};
run();
性能指标
用户关心什么
性能评估的最终目的是提升用户的使用体验。因此,性能指标的制定也要从用户的角度出发。通常来说,从网站加载到可以正常使用,我们需要关注以下几个关键节点的用户感知体验:

- 根据地址是否能正确访问到网页?服务器请求是否正常响应?
- 页面是否渲染了用户可用的、关心的内容?
- 页面是否可以正常响应用户的交互操作?
- 用户的交互响应是否流畅自然?
从这些角度出发,我们在 这里 看下 Lighthouse 提供的几个性能评估指标:

Lighthouse 最新版的提供了 6 个性能指标:FCP、SI、LCP、TTI、TBT 和 CLS;权重分别是 15%,15%,25%,15%,25% 和 5%。Lighthouse 会根据权重计算得到一个分数值。
内容呈现相关
FCP
FCP(First Contentful Paint)即首次内容绘制。它统计的是从进入页面到首次有 DOM 内容绘制所用的时间。这里的 DOM 内容指的是文本、图片、非空的 canvas
或者 SVG。我们也可以在 Performance 面板看到这个指标:

FCP 和我们常说的白屏问题相关,它记录了页面首次绘制内容的时间。一个常见的影响这个指标的问题是:FOIT(flash of invisible text,不可见文本闪烁问题),即网页使用了体积较大的外部字体库,导致在加载字体资源完成之前字体都不可见。可以通过 font-display API 来控制字体的展示来解决。
但值得注意的是,页面首次绘制的内容可能不是有意义的。比如页面绘制了一个占位的 loading 图片,这通常不是用户所关心的内容。
LCP
前面我们已经提到了,由于 FCP 指标统计的内容可能不是用户主要关心的,那么我们需要另一个指标来评估。
LCP(Largest Contentful Paint)即最大内容绘制。它统计的是从页面开始加载到视窗内最大内容绘制的所需时间,这里的内容指文本、图片、视频、非空的 canvas 或者 SVG 等。
在 LCP 之前,lighthouse 还使用过 FMP(First Meaningful Paint,首次有意义内容绘制)指标。FMP 是根据布局对象(layout objects)变化最大的时刻来决定的。但是这个指标计算比较复杂,通常和具体的页面以及浏览器的实现相关,这也会导致计算不够准确。比如,用户在某个时刻绘制了大量的小图标。
Simpler is better!用户感知网页的加载速度以及当前的可用性,可以简单地用最大绘制的元素来测量。LCP 指向的最大元素通常会随着页面的加载而变化(只在用户交互操作之前),以下是一个网站的示例:

SI
SI(Speed Index)即速度指数。Lighthouse 会在页面加载过程中捕获视频,并通过 speedline 计算视频中帧与帧之间视觉变化的进度,这个指标反映了网页内容填充的速度。页面解析渲染过程中,资源的加载和主线程执行的任务会影响到速度指数的结果。
CLS
前面几个指标关注的都是页面呈现的快慢,但是很多时候我们希望页面的视觉呈现保持相对稳定,比如,不会突然插入一张图片或者元素突然发生位移。
这个时候,Lighthouse 使用 CLS(Cumulative Layout Shift)即累计布局位移进行评估。这个指标是通过比较单个元素在帧与帧之间的位置偏移来计算,计算公式是cls = impact fraction * distance fraction
。在以下例子中,文本块在两帧之间的 impact fraction
是红色框部分,占视窗 75%;distance fraction
是蓝色箭头的距离,占视窗 25%;那么最终的分数是 0.75 * 0.25 = 0.1875
。

用户交互相关
TTI
TTI(Time To Interactive)即页面可交互的时间。这个时间的确定需要同时满足以下几个条件:
- 页面开始绘制内容,即 FCP 指标开始之后
- 用户的交互可以及时响应:
- 页面中大部分可见的元素已经注册了对应的监听事件(通常在 DOMContentLoaded 事件之后)
- 在 TTI 之后持续 5 秒的时间内无长任务执行(没有超过 50 ms 的执行任务 & 没有超过 2 个 GET 请求)

TBT
TBT(Total Blocking Time)即阻塞总时间,测量的是 FCP 与 TTI 之间的时间间隔。这个指标反映了用户的交互是否能及时响应。 如果主线程执行了长任务会导致用户的输入无法及时响应。当主线执行的任务所需的时长超过 50ms,我们就认为这是一个长任务(long task)。假设在主线程上执行了一系列的任务,每个长任务的阻塞时间等于执行时间减去 50 ms,最后可以统计得到一个总的阻塞时间。


性能优化
我们可以在报告的 Opportunities 一节看到影响评估的关键原因,并进行一些优化:

优化 JavaScript 资源加载

在 lighthouse 的优化建议中,我们可以看到和 JavaScript 资源加载相关的建议有:
- Reduce unused JavaScript
- Minify JavaScript
- Remove duplicate modules in JavaScript bundles
资源加载的优化通常有几个思路:
- 合理的加载顺序/策略(延迟加载/预先加载)
- 压缩优化资源的体积
- 代码分割 & 公共提取 & 按需加载
分析
如何确定页面中加载了哪些不必要的资源呢?我们可以打开 Chrome Devtool Coverage 面板,查看当前使用资源的代码覆盖率,红色表示未使用到的代码。

在 webpack 项目里,可使用 webpack 的插件 webpack-bundle-analyzer 来分析,如下我们启动一个本地端口来查看 webpack 的打包情况。
new BundleAnalyzerPlugin({
analyzerMode: "server",
analyzerHost: "127.0.0.1",
analyzerPort: 8888,
reportFilename: "report.html",
defaultSizes: "parsed",
openAnalyzer: true,
statsFilename: "stats.json",
statsOptions: {
exclude: ["vendor", "webpack", "hot"],
},
excludeAssets: ["webpack", "hot"],
logLevel: "info",
});
打包后会生成如下的报告,我们可以查看模块打包的情况,还可以切换 Stat
/ Parsed
/ Gizzped
来查看开启压缩后代码体积的变化:

Code Splitting
减少无用的代码,首先我们需要更加精细合理地切分代码。在 webpack 项目中,通常有三种代码分割的技巧:
- 多入口:基于 entry 配置,每个入口打包成单独的 bundle
const path = require("path");
module.exports = {
entry: "./src/index.js",
mode: "development",
entry: {
index: "./src/index.js",
another: "./src/another-module.js",
},
output: {
filename: "main.js",
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
},
};
- 抽离公共代码:如果多个页面使用了公共的模块,可以通过 SpitChunksPlugin 将代码分割成多个 chunks:
module.exports = {
//...
optimization: {
splitChunks: {
chunks: "async", // 代码分割时只对异步代码生效;all 全部生效;initial 同步代码生效
minSize: 20000, // 代码分割的最小体积,
minChunks: 1, // 做代码分割的最少引用次数
maxAsyncRequests: 30, // 同时加载模块的最大数量
cacheGroups: {
venders: {
chunks: "all",
test: /[\\/]node_modules[\\/]/, // 将 node_modules 中的代码进行分割
priority: -10, // 代码分割的优先级
reuseExistingChunk: true, // 已经打包过的代码直接复用
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
W;
- 动态加载:动态加载通常是和上面两种方法结合使用。在打包阶段,通过动态
import
引入的模块会被 webpack 单独拆分成一个chunk
;在运行的阶段,webpack 会通过chunkId
判断脚本是否已加载并缓存过,没有的话再通过script
标签动态插入。在 React 项目中,可以结合React.lazy
和Suspense
进行路由的切分。
import React, { Suspense, lazy } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
const Home = lazy(() => import("./routes/Home"));
const About = lazy(() => import("./routes/About"));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
);
DCE
摇树优化(Tree-Shaking)是用于消除死代码(Dead-Code Elimination)的一项技术。这主要是依赖于 ES6 模块的特性实现的。ES6 模块的依赖关系是确定的,和运行时状态无关,因此可以对代码进行可靠的静态分析,找到不被执行不被使用的代码然后消除。
对于无用的导入和变量,我们很容易通过 IDE 提示发现。而 webpack 会在 production
模式下自动进行一些优化,比如移除无用的导出模块:
mode: 'development',
optimization: {
usedExports: true,
},
代码压缩
terser-webpack-plugin(webpack5 自带)可以用来压缩代码和清理无意义的代码:
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
};
之前提到的都是在打包阶段进行优化。除此之外,压缩资源体积更加有效的方法是开启 gzip
,在 nginx 上我们可以很简单地进行配置:
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 9;
gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/javascript application/json;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;
考虑脚本加载的顺序
当 HTML 解析时遇到 script
脚本会暂停 DOM 树的构建,非异步的脚本需要等待浏览器下载、解析、编译和执行,这会导致渲染的延迟。因此,对于页面中脚本的引用我们需要谨慎地权衡:
- 关键的脚本内联使用,减少不必要的网络等待时间
- 其他的脚本在文档底部引入,或者通过
async/defer
异步引用
优化 CSS 资源加载

同样地,lighthouse 给出了一些 CSS 资源优化的建议:
- Reduce unused CSS
- Minify CSS
按需加载
在 webpack 中我们可以使用 mini-css-extract-plugin,将 CSS 文件从 JS 文件中单独抽取出来,支持按需加载:
new MiniCssExtractPlugin({
filename: "[name].[hash:8].css",
chunkFilename: "[name].[contenthash:8].css",
}),
资源压缩
对于 CSS 的压缩,我们可以使用 optimize-css-assets-webpack-plugin 移除无用的代码:
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.optimize\.css$/g,
cssProcessor: require("cssnano"),
cssProcessorPluginOptions: {
preset: ["default", { discardComments: { removeAll: true } }],
},
canPrint: true,
});
代码优化 & 模块化
除了借助工具来优化 CSS 资源,在开发中我们也要注意一些代码的组织和复用,例如下面这个例子:
h1 {
background-color: #000000;
}
h2 {
background-color: #000000;
}
// 减少文件代码
h1,
h2 {
background-color: #000000;
}
随着迭代的开发,项目中很可能会冗余了大量不再需要的 CSS 代码。在前端组件化的时代,将样式和组件绑定,使用更加模块化的方案或许是更加有效的策略。我们可以使用一些 CSS-in-JS 的方案,比如 styled-components,也可以了解下 Atomic CSS(原子 CSS)。
优化图片的加载

关于图片的加载,lighthouse 给出了很多方面的建议:
- Defer offscreen images
- Serve images in next-gen formats
- Efficiently encode images
- Properly size images
图片懒加载
图片的请求和加载通常需要占用较大的资源,特别是在一些以图片展示为主的电商网站上。对于当前页面不可见的图片,我们可以采用懒加载的方式进行处理。
- 我们可以通过 IntersectionObserver API 观察图片是否进入可视区域,通常需要 polyfill 处理
// html
<img data-src="http://example.com" />;
// js
const lazyLoadIntersection = new IntersectionObserver((entries) => {
entries.forEach((item, index) => {
if (item.isIntersecting) {
item.target.src = item.target.dataset.src;
lazyLoadIntersection.unobserve(item.target);
}
});
});
const imgList = document.querySelectorAll(".img-list img");
Array.from(imgList).forEach((item) => {
lazyLoadIntersection.observe(item);
});
- 我们也可以通过原生 JS 实现:
const isInView = (el) => {
const { top, left, height, width } = el.getBoundingClientRect();
const clientHeight =
window.innerHeight || document.documentElement.clientHeight;
const clientWidth = window.innerWidth;
document.documentElement.clientWidth;
if (top < clientHeight && left < clientWidth) {
return true;
}
return false;
};
const lazyload = () => {
const imgLsit = document.querySelectorAll(".img-list img");
if (!imgLsit.length) {
document.removeEventListener("scroll", throttleScroll);
return;
}
imgLsit.forEach((img) => {
if (!img.src && isInView(img)) {
img.src = img.dataset.src;
}
});
};
lazyload();
const throttleScroll = throttle(lazyload, 300);
document.addEventListener("scroll", throttleScroll, { passive: true });
响应式图片
除了懒加载,在一些响应式的网站,我们往往希望图片可以根据设备型号的不同,加载不同分辨率和大小的图片,这可以使用 Responsive images 方案来解决。主要是基于 srcset
属性:
<img srcset="elva-fairy-480w.jpg 480w,
elva-fairy-800w.jpg 800w"
sizes="(max-width: 600px) 480px,
800px"
src="elva-fairy-800w.jpg"
alt="Elva dressed as a fairy">
WebP 图片格式优化
WebP 是由谷歌(Google)推出的一种旨在加快图片加载速度的图片格式,在相同质量的情况下,WebP 的体积要比 JPEG 格式小 25% ~ 34%。目前,一些图片依托站点也提供了 webp 的支持,我们在使用之前需要检测当前浏览器的兼容情况。
<picture>
<source type="image/svg+xml" srcset="pyramid.svg">
<source type="image/webp" srcset="pyramid.webp">
<img src="pyramid.png" alt="regular pyramid built from four equilateral triangles">
</picture>
优化网络请求

针对网络连接的建立和请求的缓存,lighthouse 也给出了一些参考的建议:
- Use HTTP/2
- Serve static assets with an efficient cache policy
- Preload key requests
- Preconnect to required origins
- Avoid enormous network payloads
- Fonts with font-display: optional are preloaded
- Preload Largest Contentful Paint image
CDN 加速
我们可以将一些资源,比如图片、公共类库放到 CDN 上,基于内容分发和缓存提高响应速度,阿里云的 OSS 还提供了图片的多格式转换。
内容分发网络(Content Delivery Network,简称 CDN)是由分布在不同区域的边缘节点服务器群组成的。CDN 可以将源站内容分发到距离用户最近的节点,从而提高响应速度和成功率。其底层是依赖 DNS(域名解析服务)返回给用户最近的 IP 地址实现的。
合理的 HTTP 缓存策略
对于放置在 CDN 的资源,我们需要设置合理的 HTTP 缓存策略来资源的命中和加载。HTTP 有两种缓存机制,强缓存(基于 Expires 和 Cache-Control)以及协商缓存(基于 Last-Modified/if-Modified-Since 或 Etag / If-None-Match)。
- 对于一些不需要经常变化的资源,或者 webpack 中每次打包会根据内容生成文件路径(
contenthash
)的资源,我们可以设置一个比较大的缓存时长或者过期时间 - 而对于首页
index.html
等页面我们应该禁止缓存,或者设置一个比较短的缓存时间并检查资源的新鲜度
使用 HTTP/2
我们知道在 HTTP/1.1 中,浏览器对于同一个域名的 tcp 连接数有限制,通常只能同时发起 6 ~ 8 个连接。同时发起多个请求,在 Network 面板我们可以看到明显的阶梯式变化以及很多的 Queueing 排队等待的时间:

HTTP2 增加了多路复用、流优先级、头部压缩等功能,可以很好地解决 tcp 连接限制和队头阻塞问题。我们可以在 nginx 在配置,需要结合 https 使用:
listen 443 ssl http2;
使用 pre-* 预处理
- preload:通过
rel="preload
对优先级较高的资源进行内容预加载,比如一些重要的样式、字体文件和图片,我们不希望页面在显示的时候出现闪动,可以预加载;也可以使用 preload-webpack-plugin;
<link rel="preload" href="main.js" as="script" />
<link
rel="preload"
href="fonts/cicle_fina-webfont.ttf"
as="font"
type="font/ttf"
crossorigin="anonymous"
/>
- dns-prefetch:当页面通过网址请求服务器的时候,需要先通过 DNS 解析拿到 IP 地址才能发起请求。如果网站存在大量跨域的资源,DNS 的解析过程很可能会降低页面的性能。对于关键的跨域资源,我们最好进行 dns 预获取,还可以结合
preconnect
进行预连接:
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin />
<link rel="dns-prefetch" href="https://fonts.gstatic.com/" />
- prefetch:
preload
通常用于预加载当前页面的一些关键资源,如果我们想预获取将要用到的资源或将要导航到的页面,可以使用prefetch
;
优化页面渲染性能

我们知道,浏览器的页面渲染通常需要经过构建 DOM 树,构建 CSSOM 树,构建渲染树、样式计算和布局、绘制、合成帧并绘制到屏幕几个过程。在这个过程中,主线程复杂的 JS 任务会阻塞渲染。
针对解析和渲染的过程,lighthouse 也提出了一些优化建议:
- Avoid an excessive DOM size
- Avoid large layout shifts
- Avoid non-composited animations
- Image elements do not have explicit width and height
高性能的动画

当我们进行一些样式变换,比如改变元素的宽高时,浏览器需要重新计算元素的几何元素和样式,这个过程可能需要耗费一些时间。合成线程可以开启硬件加速且无需等待主线程计算,因此,动画应该尽可能放到合成线程处理。通常有几种方法:
- 使用
transform: scale()
代替width
和height
- 使用
transform: translate()
代替top
、right
、bottom
、left
属性
同时,要保证这些元素存在合成图层中,通常可以通过提升元素(在父元素中声明)来解决:
-
will-change: transform
transform: translate3d(0, 0, 0)
避免执行长任务
- 通过时间分片(Time Slicing),将复杂的任务拆分成多个异步执行的任务
- 将复杂的任务放到 worker 线程中,计算有了结果再通知主线程
参考资料
Lighthouse performance scoring
HTTP/2: the difference between HTTP/1.1, benefits and how to use it
How Browsers Work: Behind the scenes of modern web browsers
转:https://zhuanlan.zhihu.com/p/376925215
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律