23
头图

Lighthouse 简介

Lighthouse 是一个开源自动化工具,可以用于改进 Web 应用的质量。

Lighthouse 目前已经集成在新版本 Chrome DevTools 中,也可以将其作为一个 Chrome 扩展程序运行,或从命令行运行。

Lighthouse 会针对 Web 页面运行一些自动化测试,生成一个有关页面性能的报告,然后就可以根据测试报告来优化页面

Lighthouse 测试报告包含五块内容:性能、无障碍、最佳做法、SEO、PWA,我们今天主要是针对性能报告提出优化方案及复现代码

image.png

Chrome DevTools 可以调整语言哟,比如说从英文改成中文。

Lighthouse 性能指标

  1. First Contentful Paint
    FCP 指首次内容渲染时间,标识网页首次渲染出首个文本、图片(页面上的图像、非白色 <canvas> 元素和 SVG 被视为 DOM 内容。不包括 iframe 内的任何内容)的时间。 FCP 分位图
  2. Time to Interactive
    TTI 指可交互时间,标识网页需要多长时间才能提供完整交互功能。我们知道 JS 主线程是单线程的,如果有长 JS 任务是会阻塞页面交互
  3. Speed Index
    速度指数表明了网页内容的可见填充速度
  4. Total Blocking Time
    TBT 指累计阻塞时间,标识网页首次内容渲染 (FCP) 和可交互时间之间所有超时任务的超时累计时间,按 60FPS 算,时间应该是不超过 16ms
    超时任务指任务用时超过 50ms,如果 Lighthouse 检测到一个 70ms 长的任务,则阻塞部分将为 20ms。
  5. Largest Contentful Paint
    LCP 指 最大内容绘制,标识网页渲染出最大文本或图片的时间。类似于 First Meaningful Paint(FMP 首次有效绘制)(主要内容对用户可见的时间) 指标,但是 LCP 是一个通用固定计算规则
  6. Cumulative Layout Shift
    CLS 指累积布局偏移,标识网页可见元素在视口内的移动情况计算规则

    • 比如说在网上阅读一篇文章,结果页面上的某些内容突然发生改变?文本在毫无预警的情况下移位,导致您找不到先前阅读的位置。
    • 比如正要点击一个链接或一个按钮,但在手指落下的瞬间,诶?链接移位了,结果点到了别的东西!大多数情况下,这些体验只是令人恼火,但在某些情况下,却可能带来真正的破坏(点拒绝结果变成了允许)。
    • 或者想想变态猫?看着是平平无奇,但是操作的时候飞来横祸。

image.png

LCP、FCP 示例

图片来源:https://web.dev/lcp/#-2

image.png
image.png
image.png
image.png

Lighthouse 优化建议

建议一:减少未使用的 JavaScript、CSS 代码

image.png

手段1:异步。等需要时再加载

名词有很多:懒加载、延时加载、闲时加载、按需加载等等。

  1. 「懒加载」import 异步组件

    1. 比如说我们有一个消息中心模块,内部有用户端和管理端页面。这两个页面应该使用异步组件。因为目的不是发就是收,而且管理端是需要权限才可以看到的。
    2. 比如说我们的消息模块,支持 markdown、富文本、docs、xlsx 等等类型,但是一条消息只能显示一个类型,所以我们可以把 DocsView 来封装一下,动态加载组件渲染。
  2. 「懒加载」import 异步功能

    1. 比如说我们的表格有导出功能,基于 xlsx 实现。这样一个功能也不是高频功能,所以我们也可以通过 import 来异步载入使用。
  3. 「延时加载」通过手动调整优先级、或者延时器来实现功能。
    需要注意 CLS 指标,这里需要注意不要造成 CLS 指标异常。

    1. 还是我们消息模块的例子,在 LCP 之前我们不去加载是否有最新消息,等 LCP 之后再去加载。这里对于首屏减少了请求,对于用户的影响也会比较小 (只显示一个红点,用户有红点和无红点的点击效果都是查看消息列表)。
    2. 对于关注、点赞、收藏等开关模块需要慎重。因为效果是相反的,本来是关注,结果有可能变成了取消。

手段2:摇树优化 Tree-Shaking

可以将未使用的代码逻辑在编译时删除。
因为 Webpack 4 是默认开启状态,所以我们只说一些限制条件。

  • 只对 ES Module 起作用,对于 commonjs 无效,对于 umd 亦无效。
  • 需要包本身支持才可以。
  • 注意配置 sideEffects ,防止 Css 被优化
  • 需要 build 时才会优化。process.env.NODE_ENV === 'production' 状态

手段3:babel 降低适配版本

设置合理的 .browserslistrc,进行 babel、babel-polyfill 转义。
比如说只支持 Chrome 的后台系统,就不需要转义 IE 系列了,这样可以大大减小体积。

总结&注意事项

  1. css 的优化,也可以依赖异步组件。
  2. 三方库可以考虑使用组件做二次封装。
  3. 正确的区分 v-if 和 v-show,深入研究 el-tabs 实现机制。确保组件并没有被真实的实例化
  4. 可以通过 chrome-devtools 中 coverage 来查看哪些代码没有被执行。
    可以分析具体有哪些代码没有被执行到,我们期望的结果是加载的每一行代码都是有用的。
    可以看到有一个资源一大半代码都没有被用到,这都是浪费。如果带宽是按量付费的人得哭死。
    image.png
  5. last 2 verions 表示支持所有版本后两位。也包括永远不更新的 IE

建议二:优化体积、消除重复代码

手段1:压缩

  1. 资源服务器开始 Gzip 压缩,设置资源 30D 缓存。然后通过文件名 hash 来更新
  2. CDN 一般来说都会支持压缩和缓存,并且带宽也是比较靠谱的,节点距离用户也比较近。
  3. jsmin、摇树优化等方案。
  4. 注意图片类型。纯色图片 png,复杂图片 jpg,webp 更小。
  5. 注意图片尺寸。尽量不要过大,注意裁图。

手段2:消除重复包。依赖共享

  1. lerna + yarn
    常见重复包(axios、ui 库),因为我们好多组件都是基于业务封装(什么叫基于业务封装?内部有逻辑,存在数据调用自动更新,UI 可以直接在业务内使用)。
    我们使用了 lerna 来做相同版本共享,我们一般要求使用相同版本
  2. peerDependencies
    因为某些原因,有一些包并不是所有项目都有的,或者说对于版本有强依赖。我们也需要配置 peerDependencies让使用方可以安装正确版本的依赖
  3. externals
    部分三方库版本号是 0.xx.x,因为 lerna 是基于首位不为 0 的进行比较是否为相同版本。导致 axios@0.23axios@0.24 不认为是相同版本。
    这个时候我们会使用 externals 来强制不打包,让使用方来提供。
  4. 统一构建工具及版本
    因为有时候我们会有一些基础包(babelbabel-polyfill),但是各个 cli 版本使用的版本、方式不一样,对于转义处理也是不一致的,为了使用最小的包体积,我们要求相同相同版本的 cli webpack

手段3:替换包,选择更小的版本

  1. 小包替换大包,按需包替换全量包

    1. momentjs 改为使用 dayjsmomentjs 包只使用 format 大概在 100k 左右,而 dayjs 只有 10k 不到。
      这是因为 momentjs 里面打入了 i18n 语言包。
      所以还有另一个方案就是改成语言按需引入。
    2. 避免 import _ from 'lodash ,而是使用 lodash-es。

建议三:减少重排、减少布局变动、减少 DOM 数量

可以理解为 CLS 指标。

手段1:使用固定宽高

image.png

一般来说都有固定高度(24*24),我们把它限制在一个范围内。防止因为图片异步加载回来,撑开 dom 后,导致其后数据全部发生变动。

  1. 比如说在文章类页面中,如果有记住上次浏览位置。如何保证用户还能定位到上次位置?如果不使用固定宽高,那么用户有可能看到的内容会一直发现变化。
  2. 比如说在微信聊天页面,如何一直定位在页面最底部?当图片加载完成之后会出现高度变化。

手段2:虚拟化、虚拟列表、墓碑机制、分页加载、懒加载

虚拟列表类似实现一个最差边界方案,我只显示 20 个,这就是我性能最差的时候。不管 100 个、1000 个,我只显示 20 个。

  1. select、table、tree 等长列表注意使用虚拟列表。

    1. 比如说微信聊天,左边的会话列表大家会删除嘛?估计会有几百、几千个,包括单聊、群聊、通知、公众号等等。
      会有几个节点?头像、名称、消息摘要、时间、屏蔽、未读等等,就算会有 10 个标签。10 * 1000 这样就一万个标签了,如果使用虚拟列表,10 * 50 这样也才 500 个标签。
    2. 比如说有一些树节点,层级覆盖下去有可能会在几十万个节点,如果再操作选中之类的逻辑
  2. 异常的 tooltip 节点。

    1. 比如说会提前渲染组件。
    2. 比如说会进行频繁变更。

建议四:降低内存占用、降低 CPU 使用率

手段1:图片懒加载

图片会占用实际内存,导致卡顿。

  1. 注意图片尺寸。尽量不要过大,注意裁图。
  2. 只加载视口的图片。其他图片按需加载,参考文章页面返回上次阅读逻辑功能,如果不按需会导致无法查看图片。

手段2:减少 JS 代码、减少 CSS 代码

参考上面的逻辑就好了。

代码减少,下载、解析、执行的时候当然都会减少呀

建议您减少为解析、编译和执行 JS 而花费的时间。您可能会发现,提供较小的 JS 负载有助于实现此目标。

image.png

建议五:降低网络负载、加快用户下载速度

资源下载速度限制条件一般有什么?

  1. 带宽(吞吐)1M小水管
  2. 距离(远近)华北地方内访问、全球访问
  3. 介质(稳定)有线、无线、WiFi、5G、4G

手段1:增加带宽

需要充钱才能解决的问题都不予解决🐶

如果是服务器带宽不够,那么解决办法就是充钱。
如果是用户带宽不够,那么只能好好优化。

手段2:上 CDN

CDN 的关键技术主要有内容存储和分发技术,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

简单来说就是距离用户近,带宽大,网络畅通不拥塞。

手段3:缩小体积

(同建议一、建议二)

手段4:增加缓存

image.png

前端项目一般 html 不缓存,然后资源通过 hash name 来更新。

世界级别的难题:如何让缓存过期如何让缓存不过期

建议六:缩短执行时间、缩短执行链路、避免阻塞主线程

手段1:减少主线程工作,优化代码执行速度

  1. 正确使用循环。应该先执行 filter,再执行 map
    因为 filter 之后,数量会变少(随机数,变成一半),两个示例等于 1.5倍 和 2倍 的对比。
    那么有什么办法可以 1 倍出结果吗?(reduce?)
    image.png
  2. 正确使用循环。如果不使用结果,应该使用 forEach 遍历。
    image.png
  3. 优化嵌套循环。上面的例子充其量就是 N * 2,嵌套循环就会变成 N²。一般出现在树状结构,比如说权限
    例子采用去重来说明差距
    image.png
  4. 使用 lodash 中的方法来简化数据处理逻辑。
    可以实现代码少、效率高、语义明确。

    Object.entries(treeData).filter(([, v]) => v.level === 0).forEach(([key]) => { 这行代码想做什么?有什么性能上的问题吗?
    image.png
    Object.entries(treeData) 的性能偏差,tree 节点量级是多少?考虑用 lodash 提供的方法,性能会好一点(循环次数变少,3次降成1次)、代码变得更少了(减少了无用的语义)、语义也更加明确了(把对象转换成keyvalue数组,过滤掉有level的,遍历执行逻辑 => 遍历对象)。

手段2:减少串行代码,缩短请求链路

  1. await、async 的乱用
    image.png

建议七:避免过时的代码

Vue、react 之类框架代码还好,有统一的管理方式。常见的是一些老项目、js、jQuery 项目。

  1. 禁止 document.write
  2. 样式放上面、JS 放下面。不要阻塞 DOM 解析。

总结

优化之路,相辅相成。

  1. 优化加载速度。扩大带宽、缓存、内容分发、减小体积。
  2. 优化体积。减少损耗,减少解析时间。
  3. 优化逻辑。懒加载、异步加载、减少无效损耗、提升执行效率。

项目实战

我提供了一些最简案例在仓库中:Demo 仓库地址,你只需要看例子就可以直接看到问题(然后你可以去优化它)。


linong
29.2k 声望9.5k 粉丝

Read-Search-Ask