Lighthouse 简介
Lighthouse 是一个开源的自动化工具,可以用于改进 Web 应用的质量。
Lighthouse 目前已经集成在新版本 Chrome DevTools 中,也可以将其作为一个 Chrome 扩展程序运行,或从命令行运行。
Lighthouse 会针对 Web 页面运行一些自动化测试,生成一个有关页面性能的报告,然后就可以根据测试报告来优化页面。
Lighthouse 测试报告包含五块内容:性能、无障碍、最佳做法、SEO、PWA,我们今天主要是针对性能报告提出优化方案及复现代码。
Chrome DevTools 可以调整语言哟,比如说从英文改成中文。
Lighthouse 性能指标
- First Contentful Paint
FCP 指首次内容渲染时间,标识网页首次渲染出首个文本、图片(页面上的图像、非白色元素和
SVG
被视为 DOM 内容。不包括 iframe 内的任何内容)的时间。 FCP 分位图。 - Time to Interactive
TTI 指可交互时间,标识网页需要多长时间才能提供完整交互功能。我们知道 JS 主线程是单线程的,如果有长 JS 任务是会阻塞页面交互 - Speed Index
速度指数表明了网页内容的可见填充速度 - Total Blocking Time
TBT 指累计阻塞时间,标识网页首次内容渲染 (FCP) 和可交互时间之间的所有超时任务的超时累计时间,按 60FPS 算,时间应该是不超过 16ms
超时任务指任务用时超过 50ms,如果 Lighthouse 检测到一个 70ms 长的任务,则阻塞部分将为 20ms。 - Largest Contentful Paint
LCP 指 最大内容绘制,标识网页渲染出最大文本或图片的时间。类似于 First Meaningful Paint(FMP 首次有效绘制)(主要内容对用户可见的时间) 指标,但是 LCP 是一个通用固定计算规则。 Cumulative Layout Shift
CLS 指累积布局偏移,标识网页可见元素在视口内的移动情况。计算规则- 比如说在网上阅读一篇文章,结果页面上的某些内容突然发生改变?文本在毫无预警的情况下移位,导致您找不到先前阅读的位置。
- 比如正要点击一个链接或一个按钮,但在手指落下的瞬间,诶?链接移位了,结果点到了别的东西!大多数情况下,这些体验只是令人恼火,但在某些情况下,却可能带来真正的破坏(点拒绝结果变成了允许)。
- 或者想想变态猫?看着是平平无奇,但是操作的时候飞来横祸。
LCP、FCP 示例
Lighthouse 优化建议
建议一:减少未使用的 JavaScript、CSS 代码
手段1:异步。等需要时再加载
名词有很多:懒加载、延时加载、闲时加载、按需加载等等。
「懒加载」import 异步组件
- 比如说我们有一个消息中心模块,内部有用户端和管理端页面。这两个页面应该使用异步组件。因为目的不是发就是收,而且管理端是需要权限才可以看到的。
- 比如说我们的消息模块,支持 markdown、富文本、docs、xlsx 等等类型,但是一条消息只能显示一个类型,所以我们可以把 DocsView 来封装一下,动态加载组件渲染。
「懒加载」import 异步功能
- 比如说我们的表格有导出功能,基于 xlsx 实现。这样一个功能也不是高频功能,所以我们也可以通过 import 来异步载入使用。
「延时加载」通过手动调整优先级、或者延时器来实现功能。
需要注意 CLS 指标,这里需要注意不要造成 CLS 指标异常。- 还是我们消息模块的例子,在 LCP 之前我们不去加载是否有最新消息,等 LCP 之后再去加载。这里对于首屏减少了请求,对于用户的影响也会比较小 (只显示一个红点,用户有红点和无红点的点击效果都是查看消息列表)。
- 对于关注、点赞、收藏等开关模块需要慎重。因为效果是相反的,本来是关注,结果有可能变成了取消。
手段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 系列了,这样可以大大减小体积。
总结&注意事项
- css 的优化,也可以依赖异步组件。
- 三方库可以考虑使用组件做二次封装。
- 正确的区分 v-if 和 v-show,深入研究 el-tabs 实现机制。确保组件并没有被真实的实例化
- 可以通过 chrome-devtools 中 coverage 来查看哪些代码没有被执行。
可以分析具体有哪些代码没有被执行到,我们期望的结果是加载的每一行代码都是有用的。
可以看到有一个资源一大半代码都没有被用到,这都是浪费。如果带宽是按量付费的人得哭死。
last 2 verions
表示支持所有版本后两位。也包括永远不更新的 IE
建议二:优化体积、消除重复代码
手段1:压缩
- 资源服务器开始 Gzip 压缩,设置资源 30D 缓存。然后通过文件名 hash 来更新
- CDN 一般来说都会支持压缩和缓存,并且带宽也是比较靠谱的,节点距离用户也比较近。
- jsmin、摇树优化等方案。
- 注意图片类型。纯色图片 png,复杂图片 jpg,webp 更小。
- 注意图片尺寸。尽量不要过大,注意裁图。
手段2:消除重复包。依赖共享
lerna + yarn
常见重复包(axios、ui 库),因为我们好多组件都是基于业务封装(什么叫基于业务封装?内部有逻辑,存在数据调用自动更新,UI 可以直接在业务内使用)。
我们使用了 lerna 来做相同版本共享,我们一般要求使用相同版本。peerDependencies
因为某些原因,有一些包并不是所有项目都有的,或者说对于版本有强依赖。我们也需要配置peerDependencies
来让使用方可以安装正确版本的依赖。externals
部分三方库版本号是0.xx.x
,因为lerna
是基于首位不为 0 的进行比较是否为相同版本。导致[email protected]
和[email protected]
不认为是相同版本。
这个时候我们会使用externals
来强制不打包,让使用方来提供。- 统一构建工具及版本
因为有时候我们会有一些基础包(babel
、babel-polyfill
),但是各个cli
版本使用的版本、方式不一样,对于转义处理也是不一致的,为了使用最小的包体积,我们要求相同相同版本的cli
webpack
。
手段3:替换包,选择更小的版本
小包替换大包,按需包替换全量包
momentjs
改为使用dayjs
,momentjs
包只使用format
大概在100k
左右,而dayjs
只有10k
不到。
这是因为momentjs
里面打入了i18n
语言包。
所以还有另一个方案就是改成语言按需引入。- 避免 import _ from 'lodash ,而是使用 lodash-es。
建议三:减少重排、减少布局变动、减少 DOM 数量
可以理解为 CLS 指标。
手段1:使用固定宽高
一般来说都有固定高度(24*24
),我们把它限制在一个范围内。防止因为图片异步加载回来,撑开 dom 后,导致其后数据全部发生变动。
- 比如说在文章类页面中,如果有记住上次浏览位置。如何保证用户还能定位到上次位置?如果不使用固定宽高,那么用户有可能看到的内容会一直发现变化。
- 比如说在微信聊天页面,如何一直定位在页面最底部?当图片加载完成之后会出现高度变化。
手段2:虚拟化、虚拟列表、墓碑机制、分页加载、懒加载
虚拟列表类似实现一个最差边界方案,我只显示 20 个,这就是我性能最差的时候。不管 100 个、1000 个,我只显示 20 个。
select、table、tree 等长列表注意使用虚拟列表。
- 比如说微信聊天,左边的会话列表大家会删除嘛?估计会有几百、几千个,包括单聊、群聊、通知、公众号等等。
会有几个节点?头像、名称、消息摘要、时间、屏蔽、未读等等,就算会有 10 个标签。10 * 1000
这样就一万个标签了,如果使用虚拟列表,10 * 50
这样也才 500 个标签。 - 比如说有一些树节点,层级覆盖下去有可能会在几十万个节点,如果再操作选中之类的逻辑
- 比如说微信聊天,左边的会话列表大家会删除嘛?估计会有几百、几千个,包括单聊、群聊、通知、公众号等等。
异常的 tooltip 节点。
- 比如说会提前渲染组件。
- 比如说会进行频繁变更。
建议四:降低内存占用、降低 CPU 使用率
手段1:图片懒加载
图片会占用实际内存,导致卡顿。
- 注意图片尺寸。尽量不要过大,注意裁图。
- 只加载视口的图片。其他图片按需加载,参考文章页面返回上次阅读逻辑功能,如果不按需会导致无法查看图片。
手段2:减少 JS 代码、减少 CSS 代码
参考上面的逻辑就好了。
代码减少,下载、解析、执行的时候当然都会减少呀。
建议您减少为解析、编译和执行 JS 而花费的时间。您可能会发现,提供较小的 JS 负载有助于实现此目标。
建议五:降低网络负载、加快用户下载速度
资源下载速度限制条件一般有什么?
- 带宽(吞吐)1M小水管
- 距离(远近)华北地方内访问、全球访问
- 介质(稳定)有线、无线、WiFi、5G、4G
手段1:增加带宽
需要充钱才能解决的问题都不予解决
如果是服务器带宽不够,那么解决办法就是充钱。
如果是用户带宽不够,那么只能好好优化。
手段2:上 CDN
CDN 的关键技术主要有内容存储和分发技术,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
简单来说就是距离用户近,带宽大,网络畅通不拥塞。
手段3:缩小体积
(同建议一、建议二)
手段4:增加缓存
前端项目一般 html 不缓存,然后资源通过 hash name 来更新。
世界级别的难题:如何让缓存过期和如何让缓存不过期。
建议六:缩短执行时间、缩短执行链路、避免阻塞主线程
手段1:减少主线程工作,优化代码执行速度
- 正确使用循环。应该先执行
filter
,再执行map
。
因为 filter 之后,数量会变少(随机数,变成一半),两个示例等于 1.5倍 和 2倍 的对比。
那么有什么办法可以 1 倍出结果吗?(reduce?)
- 正确使用循环。如果不使用结果,应该使用 forEach 遍历。
- 优化嵌套循环。上面的例子充其量就是 N * 2,嵌套循环就会变成 N²。一般出现在树状结构,比如说权限。
例子采用去重来说明差距
使用 lodash 中的方法来简化数据处理逻辑。
可以实现代码少、效率高、语义明确。Object.entries(treeData).filter(([, v]) => v.level === 0).forEach(([key]) => {
这行代码想做什么?有什么性能上的问题吗?
Object.entries(treeData)
的性能偏差,tree
节点量级是多少?考虑用 lodash 提供的方法,性能会好一点(循环次数变少,3次降成1次)、代码变得更少了(减少了无用的语义)、语义也更加明确了(把对象转换成keyvalue数组,过滤掉有level的,遍历执行逻辑 => 遍历对象)。
手段2:减少串行代码,缩短请求链路
- await、async 的乱用
建议七:避免过时的代码
Vue、react 之类框架代码还好,有统一的管理方式。常见的是一些老项目、js、jQuery 项目。
- 禁止
document.write
- 样式放上面、JS 放下面。不要阻塞 DOM 解析。
总结
优化之路,相辅相成。
- 优化加载速度。扩大带宽、缓存、内容分发、减小体积。
- 优化体积。减少损耗,减少解析时间。
- 优化逻辑。懒加载、异步加载、减少无效损耗、提升执行效率。
项目实战
我提供了一些最简案例在仓库中:Demo 仓库地址,你只需要看例子就可以直接看到问题(然后你可以去优化它)。