Lighthouse 前端性能优化:从一个DEMO项目入手,一步一步提升性能评分

2,025 阅读14分钟

前言

2024 年的今天,Lighthouse 已成为衡量和提升网页性能的首选工具。然而,对于新手开发者而言,初次使用 Lighthouse 时往往感到迷茫,不知从何入手。

因此,我在 GitHub 上基于 React 和 Webpack 搭建了一个 Demo 项目,模拟了现实中可能遇到的性能问题(为了更加凸显问题刻意的写了一些额外的代码和配置),并逐步展示优化过程。在这个项目中的 commit 记录中,我详细记录了如何将 Lighthouse 性能评分从 2分 提升到 99分 的过程。涵盖了代码压缩、图片优化、预加载、懒加载等常见优化技巧。

项目地址:lighthouse-demo

你可以将这个项目克隆到本地,并切到 before_optimization 分支,尝试自己进行优化,看看能将分数提升到多少。然后,跟随这篇文章和 commit 记录,一步一步提升 Lighthouse 评分。

优化措施Lighthouse 评分相关 Commit
初始页面2 分init: initial project
开启文本压缩21 分perf: enable text compression
优化图片偏移52 分perf: optimize CLS by setting image size
延迟加载 JS 脚本57 分perf: defer loading JS scripts
减少 JS 执行时间86 分perf: reduce JS execution time
分包和提取 CSS 文件86 分perf: split chunks and extract CSS
缩减资源体积97 分perf: reduce resource size
图片优化99 分perf: optimize images
延迟加载 CSS 文件99 分perf: defer loading CSS files

0. 本地启动项目

  1. 克隆项目到本地
git clone git@github.com:leowux/lighthouse-demo.git
  1. 切换到 before_optimization 分支
git checkout before_optimization
  1. 安装依赖
npm install
  1. 预览项目(打包+启动)
npm run preview
  1. 打开 Chrome Devtool 工具,切到 Lighthouse 栏,点击分析页面按钮。

image.png

等待些许时间就可以看到性能评分了:

image.png

  1. 修改代码后,需要重新执行 npm run preview,在进行 lighthouse 性能分析。

1. 开启文本压缩

相关 Commit:perf: enable text compression

在 Lighthouse 评分界面往下滑,就可以发现官方提供的一些诊断建议(DIAGNOSTICS),我们可以参照这些建议进行优化。 如下图,Lighthouse 建议开启文本压缩以缩减网络资源的体积,从而提升资源的加载速度。

image.png

打开 Demo 项目我们可以发现,项目是通过 express 启动的服务,因此我们只需要安装 compression 依赖,然后在 server.js 文件通过 app.use 添加文本压缩的中间件 compression 即可。

image.png 再次运行 npm run preview,分析页面,结果分数从 2 分提升到 21 分。可以发现跟资源加载速度相关的指标,FCP(首次内容绘制时间)、LCP(最大内容绘制时间)以及 Speed Index(速度指标)都有相应的提升。

image.png

2. 优化图片偏移

相关 Commit:perf: avoid large layout shifts

接下来 Lighthouse 诊断出页面存在严重的布局偏移问题,并帮我定位了问题所在的图片元素。

image.png

首先简单介绍一下什么是布局偏移,以及相关的 Lighthouse 指标 CLS(累积布局偏移):

布局偏移:当用户浏览网页时,页面上的元素(例如文本、图片、按钮)如果在加载过程中发生了意外位置变化,就会产生布局偏移。这样的情况可能会导致用户体验不佳,例如点击某个按钮时,由于按钮位置变化而点到了别的地方。

CLS:该指标衡量的是网页整个生命周期内所有意外布局偏移的总和。CLS 的值越大,说明页面的视觉稳定性越差。

常见的导致布局偏移原因以及解决方案:

  • 图片没有指定尺寸,导致加载时内容位置变化;
    • 解决方案:在 <img> 标签中明确指定 widthheight 属性;
  • 动态加载的内容(如广告)没有预留空间;
    • 解决方案:为广告、动态内容等预留固定的占位元素;
  • 使用字体时,字体加载后替换了默认字体;
    • 解决方案:使用 font-display: swap 避免字体加载时的布局抖动。

回到 Demo 项目,Lighthouse 已经帮我们定位到发生布局偏移的图片。查看代码发现:图片没有指定固定的高度属性,我们给 img 标签添加 height 属性即可。

image.png

优化后分数从 21 分提升到 52 分,与之相关的 CLS 指标从 0.825 降低到了 0。可以看出 LCS 在整个 Lighthouse 性能评分占据着较大的比重(25%)。

image.png

3. 延迟加载JS脚本

相关 Commit:perf: defer script

下图中 Lighthouse 诊断出:存在阻塞页面渲染的资源,并且建议延迟加载所有非必要资源( Resources are blocking the first paint of your page. Consider delivering critical JS/CSS inline and deferring all non-critical JS/styles)

image.png

在 Lighthouse 面板中,资源对渲染的影响可能并不直观。我们可以借助 Performance 工具对页面进行录制,在录制完成后查看性能报告中的 Main 视图(主线程),来清晰地呈现浏览器的渲染情况:

image.png

在上图 Performance 面板我们可以看到,JS 资源的加载和执行阻塞了页面渲染,导致页面首次渲染的时间延后了很长时间。这里涉及到浏览器在加载页面时的一个机制:在解析 HTML 过程中,如果遇到同步的 JS 脚本,会优先加载并且执行它,执行完毕后再去解析 HTML。针对这种情况,我们可以给 script 标签添加 defer 属性,让其异步加载 JS 资源,并且等待 HTML 解析完毕后再去执行 JS 脚本。

image.png

回到 Demo 项目,页面的 script 标签是通过 webpackHtmlWebpackPlugin 插件插入 HTML 中的,因此我们需要将插件的 scriptLoading 属性从 blocking 修改为 defer,就可以实现 JS 脚本的延迟加载。

image.png

优化后分数从 52 分提升到 57 分,并且与页面渲染速度相关的指标 FCP 和 LCP 都有相应的提升。

image.png

4. 减少JS执行时间

相关 Commit:perf: reduce javascript execution time

下图中 Lighthouse 建议减少 JS 的执行时间,由于 JavaScript 是单线程,在处理耗时较长的任务时就没办法及时响应用户操作,从而影响到一个非常重要的指标——TBT(总阻塞时间)。

image.png

简单介绍一下 TBT 指标:

  • TBT 衡量的是网页未响应用户输入(例如鼠标点击、屏幕点按或键盘按下操作)的总时长。
  • 计算规则:将 FCP 和 TTI 之间所有长任务阻塞部分相加。(任何执行时间超过 50 毫秒的任务都是长任务。50 毫秒之后的时间属于阻塞部分。例如,如果 Lighthouse 检测到时长为 70 毫秒的任务,则阻塞部分将为 20 毫秒。)
  • 常见优化手段:
    • 减少 JavaScript 执行时间,优化业务逻辑代码,去掉无效代码,使用更高效的算法或数据结构,避免重复计算或不必要的操作。
    • 最小化主线程工作,将一些耗时的任务分片执行,利用 requestIdleCallback 或 setTimeout 将任务拆分为多个小于50毫秒的子任务,或者将任务移动到 Web Worker 中执行。

那么如何定位长任务?我们可以继续使用 Performance 工具 对页面进行录制和分析。若 Main 栏的任务出现红色条,则表明这是个长任务。

image.png

由于代码经过了打包,导致我们无法直接定位到代码的具体位置,有需求的同学可以通过 source map 精确定位。我们直接来到项目的主组件 src/App.tsx,可以发现这里存在一个复杂计算,每次渲染 App 组件是都会重复进行计算。这个计算值不依赖于任何状态,所以我们可以直接计算出结果,避免计算导致的长任务耗时。(如果依赖前端状态可以用 useMemo 进行缓存,避免重复计算。)

image.png

优化后分数从 57 分提升到 86 分,TBT 阻塞时间从 9390ms 减少至 190ms,有明显的提升。

image.png

5. 分包和提取CSS文件

相关 Commit:perf: split chunks and extract css

在优化完 JS 执行时间后,细心的朋友也许会发现,TBT 阻塞时间还有 190s。我们再次打开 Performance 面板进行分析,发现脚本评估(Evaluate Script)占据着主线程大部分时间,且大于脚本执行(Function call)的时间。

image.png

  • 脚本评估是什么?脚本评估时,系统会先对其进行解析以检查是否存在错误。如果解析器未发现错误,则脚本会编译为字节码然后可以继续执行。
  • 如何优化脚本评估?我们可以通过拆分脚本,将脚本评估工作分散到许多较小的任务中,从而减少长任务的阻塞时间,降低 TBT 指标。

首先,打开 Network 看一下资源的组成:

image.png 可以发现除了图片资源和页面 document 之外,只有一个 JS 资源。我们再来分析一下 JS 资源的构成,打开本地终端,运行脚本分析命令:

npm run analyze

image.png

我们发现样式文件和一些第三方 JS 依赖(antd、moment、react-dom等)都被打进 main.js 了,导致 JS 资源的体积较大,有 367 kb。

分离 JS Bundle

首先我们先利用 Webpack 自带的 splitChunks 分包功能,将第三方 JS 依赖分离成单独的 JS Bundle:

image.png 在 Webpack 配置文件 scripts/webpack.base.js 中,通过 optimization.splitChunks 属性,我们可以配置 JS 的分包规则:

  • chunks 属性设置为 all,表示对所有(异步&同步)文件提取公共部分,进行分包;
  • cacheGroups 属性用来设置缓存组,可以对特定路径下的文件进行分包。

提取 CSS 文件

除了第三方 JS 依赖之外,目前样式文件也被打进了 main.js 里,我们可以通过 mini-css-extract-plugin 插件将样式文件抽离成单独的 css 文件。

image.png

最终,我们将 main.js 文件拆分成了多个单独的 js 文件和 css 文件:

image.png

做完分包以及 CSS 文件提取之后,TBT 的阻塞时间从 190ms 降低到 30ms,缩短了脚本评估的时间。但是总分却没有变化,LCP 反而增加了,其原因是单独抽离 CSS 资源阻塞了 HTML 的解析和渲染(后面会详细阐述)。同时资源的总体积并没有缩减,所以 Lighthosue 的分数变化不大。

image.png

6. 缩减资源体积

相关 Commit:perf: import antd on demand & perf: dayjs replace momentjs

完成分包后,继续使用 Lighthouse 分析页面:

image.png

经过诊断,Lighthouse 建议减少未使用的 JS 和 CSS(Reduce unused JavaScript & CSS),并且定位到具体的位置:antd-icon.jsmoment.jsantd.css

通过 Network 面板,我们也可以直观的看到 antd-icon.jsantd.css 资源体积都比较大。

image.png

针对 antd(3.x版本) 我们可以直接通过具体的文件路径引入组件、图标和样式,以实现按需引入:

image.png

同时,针对 moment(75.9kb)库,我们可以采用功能基本相同,但是体积更小的 dayjs(2kb)进行替换:

image.png

经过优化后,JS 和 CSS 资源的体积都得到了削减:

image.png

优化过资源体积后,Lighthouse 评分从 86 分提升到了 97 分,TBT 和 LCP 指标都有显著的提升。

image.png

7. 图片优化

相关 Commit:perf: optimize images & perf: preload largest content image

优化完 JS 和 CSS 资源后,Lighthouse 下一步的诊断建议是针对图片进行优化:提供下一代的图片格式以及预加载 LCP 图片(Serve images in next-gen formats & Preload Largest Contentful Paint image)

image.png

优化图片格式

目前项目使用的 .jpg 格式的图片,Lighthouse 建议使用 .webp 格式的图片。

WebP 是一种现代图片格式,可为网络上的图片提供出色的无损和有损压缩。使用 WebP,网站站长和 Web 开发者可以创建更小、更丰富的图片,从而提高网页加载速度。

我们可以通过线上图片转换网站 JPG to WEBP | CloudConvert 将图片转化为 .webp 格式,然后在项目 assets/images 目录下进行替换。

image.png

根据上图,可以看到图片资源的体积在转化为 webp 格式后削减了一半以上,优化效果还是十分明显的。

预加载 LCP 图片

LCP 资源指的是页面中最大的可见内容元素(通常是图片,视频或文本),对于本项目来说,LCP 资源是页面最开头的三张松鼠图片的其中之一。一般来说,我们希望 LCP 资源与在网页加载的第一个资源的同时开始加载。换句话说,如果 LCP 资源的加载时间晚于第一个资源,则说明有改进的空间。

image.png

针对本项目,我们可以通过 <link ref="preload" href="..." as="image" /> 标签预加载 LCP 图片资源:

image.png

预加载优化完毕,我们再来通过 Performance 面板对比一下优化前后 LCP 资源加载顺序的区别:

优化前,LCP 图片资源在 JS/CSS 资源加载完成后再加载:

image.png

优化后,LCP 图片资源与 JS/CSS 同步加载:

image.png

最终,经过图片格式优化和预加载优化后,评分从 97 分提高到 99 分, LCP 从 2.6s 缩减到 2.0s。

image.png

8. 延迟加载CSS文件

相关 Commit:perf: defer css and split chunks

经过上文一系列的优化,目前页面的性能已经变得十分可观。Lighthouse 的诊断建议只剩下一项:消除阻塞渲染的资源(Eliminate render-blocking resources):

image.png

通过诊断信息我们可以知道:CSS 资源 main.css 的加载阻塞了渲染。通过下图 Performance 面板 Network 栏,我们也可以看到 main.css 资源被标记了 Render blocking 标识。

image.png

上文中提到过 <script /> 脚本标签可以通过添加 defer 属性实现延迟加载的功能,但对于 <link ref="stylesheet" /> 样式标签实现延迟加载会相对复杂一些,但我们仍然可以通过以下的方式实现:

  1. rel="stylesheet" 修改为 rel="preload",并添加 as="style",这样就可以让浏览器以为这是一个异步加载资源;
  2. 添加 onload="this.onload=null;this.rel='stylesheet'" 回调事件,这样可以在资源加载完后,使浏览器以样式文件的格式进行解析;

又因为 link 标签是由 mini-css-extract-plugin 生成的,但是这个插件不提供修改 link 标签属性的方法,所以为了实现上述的 1、2 步骤,我们需要自己实现一个 Webpack Plugin

实现思路如下:利用 HtmlWebpackPluginbeforeEmit 钩子,获取当前的 HTML 内容,然后给 link 的标签替换上述 1、2 步骤中的属性。

具体实现代码如下(代码地址见本段文章开头相关 Commit):

image.png

通过上述比较 Hack 的方案,成功实现了 main.css 资源的延迟加载(defer loading),Render blocking 标识被成功的去除:

image.png

最终,Lighthouse 分数定格在 99 分,红色的诊断建议也清理完毕!🎉🎉🎉

image.png

image.png

总结

通过上述优化过程,我们可以将优化手段分为以下几个方面:

  1. 资源加载优化

    • 开启文本压缩减少资源体积(使用 compression 库);
    • 延迟加载 JS 脚本(使用 script defer 关键字);
    • 延迟加载 CSS 文件(通过修改 link 标签属性);
    • 预加载关键资源(preload LCP 图片);
  2. 图片相关优化

    • 避免图片布局偏移(设置固定尺寸);
    • 使用下一代的图片格式(.jpg 替换为 .webp 格式);
  3. 代码优化

    • 代码分割(使用 Webpack 的 splitChunks 功能)
    • 按需引入(通过具体的文件路径引用组件)
    • 减少代码执行时间(分割长任务和优化昂贵计算)
    • 寻找体积更小的第三方依赖(dayjs 替换 momentjs

通过这些优化手段,我们成功将 Lighthouse 性能评分从 2 分提升到 99 分。具体指标改善包括:

  • FCP(首次内容绘制)从 3.7s 降至 0.8s;
  • LCP(最大内容绘制)从 8.4s 降至 2.0s;
  • CLS(累积布局偏移)从 0.825 降至 0;
  • TBT(总阻塞时间)从 9400ms 降至 0ms;

这些优化不仅提升了性能评分,更重要的是显著改善了用户体验。希望这个示例项目能帮助大家更好地理解和实践网页 Lighthouse 性能优化。