性能优化经验分享
The following article is from 字节前端 ByteFE Author linyangxin
背景
近期,开发 C 端 h5 页面时,发现首页白屏时间比较长,并且用户也多次反映了这个问题,优化这个首屏加载时间是迟早的事,所以在开始优化前先做一些必要的知识储备~
性能指标
还在看那些老掉牙的性能优化文章么?这些最新性能指标了解下[1]
FP & FCP
首次绘制,FP(First Paint),这个指标用于记录页面第一次绘制像素的时间。
首次内容绘制,FCP(First Contentful Paint),这个指标用于记录页面首次绘制文本、图片、非空白 Canvas 或 SVG 的时间。
FP 指的是绘制像素,比如说页面的背景色是灰色的,那么在显示灰色背景时就记录下了 FP 指标。但是此时 DOM 内容还没开始绘制,可能需要文件下载、解析等过程,只有当 DOM 内容发生变化才会触发,比如说渲染出了一段文字,此时就会记录下 FCP 指标。因此说我们可以把这两个指标认为是和白屏时间相关的指标,所以肯定是最快越好。
LCP
最大内容绘制,LCP(Largest Contentful Paint),用于记录视窗内最大的元素绘制的时间,该时间会随着页面渲染变化而变化,因为页面中的最大元素在渲染过程中可能会发生改变,另外该指标会在用户第一次交互后停止记录。指标变化如下图:
TTI
介绍 TTI 之前,首先要介绍一下长任务,一个任务的耗时超过 50ms,这个任务就可以被认为是长任务,用户的交互操作也是在主线程执行的,所以当发生 Long Task 时,用户的交互操作很可能无法及时执行,这时用户就会体验到卡顿(当页面响应时间超过 100ms 时,用户可以体验到卡顿)。
首次可交互时间,TTI(Time to Interactive),测量页面所有资源加载成功并能够可靠地快速响应用户输入的时间。通常是发生在页面依赖的资源已经加载完成,此时浏览器可以快速响应用户交互的时间。指标的计算过程,需要满足以下几个条件:
从 FCP 指标后开始计算; 持续 5 秒内无长任务(执行时间超过 50 ms)且无两个以上正在进行中的 GET 请求; 往前回溯至 5 秒前的最后一个长任务结束的时间。
FID
首次输入延迟,FID(First Input Delay),记录在 FCP 和 TTI 之间用户首次与页面交互时响应的延迟。记录第一次与页面交互到浏览器真正能够处理响应该交互的时间,这个延迟出现的原因是浏览器的主线程可能在忙于其他工作,比如解析 JS 文件等,所以无法及时响应用户。
TBT
阻塞总时间,TBT(Total Blocking Time),记录在 FCP 到 TTI 之间所有长任务的阻塞时间总和。主线程执行的任务分为长任务和短任务。规定持续时间超过 50ms 的任务为长任务,低于 50ms 的任务为短任务。长任务中超过 50ms 的时间被认为是“阻塞”的,因此,TBT 是所有长任务中阻塞时间的总和。TBT = FCP 和 TTI 之间发生的每个长任务的「阻塞时间」总和。例:
上图,有三个长任务,两个短任务。
而 TBT 时长为 200+40+105=345ms。
CLS
累计位移偏移,CLS(Cumulative Layout Shift),记录了页面上非预期的位移波动。比如:页面渲染过程中突然插入一张巨大的图片或者说点击了某个按钮突然动态插入了一块内容等等相当影响用户体验的网站。这个指标就是为这种情况而生的,计算方式为:位移影响的面积 * 位移距离。
以上图为例,文本移动了 25% 的屏幕高度距离(位移距离),位移前后影响了 75% 的屏幕高度面积(位移影响的面积),那么 CLS 为 0.25 * 0.75 = 0.1875
。CLS 推荐值为低于 0.1,越低说明页面跳来跳去的情况就越少,用户体验越好。毕竟很少有人喜欢阅读或者交互过程中网页突然动态插入 DOM 的情况,比如说插入广告。
关键指标
LCP 代表了页面的速度指标,虽然还存在其他的一些体现速度的指标,但是上文也说过 LCP 能体现的东西更多一些。一是指标实时更新,数据更精确,二是代表着页面最大元素的渲染时间,通常来说页面中最大元素的快速载入能让用户感觉性能还挺好。 FID 代表了页面的交互体验指标,毕竟没有一个用户希望触发交互以后页面的反馈很迟缓,交互响应的快会让用户觉得网页挺流畅。 CLS 代表了页面的稳定指标,尤其在手机上这个指标更为重要。因为手机屏幕挺小,CLS 值一大的话会让用户觉得页面体验做得很差。
优化手段
网络层面
开启 gzip 压缩
Vue 项目性能优化 — 实践指南(网上最全 / 详细)[2]
gzip 是 GNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE 等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,gzip 压缩效率非常高,通常可以达到 70% 的压缩率。
使用 http2
HTTP2.0 新特性[3]
HTTP2.0 大幅度提高了 web 性能,在 HTTP1.1 完全语义兼容的基础上,进一步减少了网络的延迟。实现低延迟高吞吐量。对于前端开发者而言,减少了优化工作。Http2 提供了以下一些新特性,对性能优化有一定帮助。
二进制分 帧:将所有传输信息分割为更小的消息和帧,并对它们采用二进制格式的编码将其封装。二进制分帧主要是为下文中的各种特性提供了基础。它能把一个数据划分封装为更小更便捷的数据。首先是在单链接多资源方式中,减少了服务端的链接压力,内存占用更少,链接吞吐量更大。这一点可以结合下文中的多路复用来体会。另一方面,由于 TCP 链接的减少而使网络拥塞状态得以改善,同时慢启动时间的减少。使拥塞和丢包恢复的速度更快。 多路复用:基于二进制分帧层,HTTP2.0 可以在共享 TCP 链接的基础上同时发送请求和响应。HTTP 消息被分解为独立的帧,而不破坏消息本身的语义,交错发出去,在另一端根据流标识符和首部将他们重新组装起来。可以并行交错的发送请求和响应,这些请求和响应之间互不影响,消除不必要的延迟,减少页面加载时间。 首部压缩:对于相同的数据,不再重新通过每次请求和响应发送。每个新的首部键值对要么追加到当前表的末尾,要么替换表中之前的值。首部压缩可以使报头更紧凑,更快速传输,有利于移动网络环境。减少每次通讯的数据量,使网络拥塞状态得以改善。 优先级:客户端明确指定优先级,服务端可以根据这个优先级作为交互数据的依据,优先将最高优先级的帧发送给客户端。 流量控制:由于一个 TCP 连接流量带宽(根据客户端到服务器的网络带宽而定)是固定的,当有多个请求并发时,一个请求占的流量多,另一个请求占的流量就会少。流量控制可以对不同的流的流量进行精确控制。 服务器推送:服务端根据客户端的请求,提前返回多个响应,推送额外的资源给客户端。服务端推送是一种在客户端请求之前发送数据的机制。在 HTTP2.0 中,服务器可以对一个客户端的请求发送多个响应。如果一个请求是由你的主页发送的,服务器可能会响应主页内容、logo 以及样式表,因为他知道客户端会用到这些东西。这样不但减轻了数据传送冗余步骤,也加快了页面响应的速度,提高了用户体验。
iconfont 代替图片图标
字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等,非常方便,并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小,无论是加载还是打包所消耗的资源都相对较小一些。
图片优化
WebP 相对于 PNG、JPG 有什么优势?[4]
图片往往是一个 h5 页面的重要组成部分,然而图片占用的资源往往也是很大的,因此图片优化在性能优化中占据很重要的地位。下面来看几种优化图片的方式。
图片懒加载:当图片出现在可视区域或者即将出现在可视区域时再加载图片,避免一次性加载全部图片,会对用户体验造成很大影响。 降低图片质量:一些图片适当降低图片质量时,通常是看不出来区别的,尤其是作为背景图片时,可以使用 image-webpack-loader
进行图片压缩。尽量使用 CSS 代替图片:一些简单的图片效果如果可以通过 CSS 效果实现则进行用 CSS 来实现,可以减小请求次数或者打包体积大小。 使用 webp 图片:WebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。
按需加载
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
代码层面
location.herf
在教师端项目中登录重定向部分,由于历史代码逻辑问题,无法使用 react router 中的跳转方式,所以选择了使用location.herf
进行跳转。虽然,功能上没有问题,但是由于页面在登录过程中频繁地重新加载,导致登录重定向的过程十分缓慢,极大影响了用户体验,所以考虑将登录重定向部分代码进行重构,避免使用location.herf
。
CSS 策略
CSS 性能优化的几个技巧[5]
想要优化 CSS 的性能,我们首先需要了解 CSS 的渲染规则,CSS 选择器是从右向左进行匹配的。 CSS 中更多的选择器是不会匹配的,所以在考虑性能问题时,需要考虑的是如何在选择器不匹配时提升效率。从右向左匹配就是为了达成这一目的的,通过这一策略能够使得 CSS 选择器在不匹配的时候效率更高。这样想来,在匹配时多耗费一些性能也能够想的通了。
避免出现超过三层的嵌套规则:元素的嵌套层级不能超过 3 级,过度的嵌套会导致代码变得臃肿,沉余,复杂。导致 css 文件体积变大,造成性能浪费,影响渲染的速度!而且过于依赖 HTML 文档结构。这样的 css 样式,维护起来,极度麻烦。 避免为 ID 选择器添加多余选择器:在 ID 选择器前面嵌套其它选择器纯粹是多余的。 避免使用通配选择器,只对目标节点声明规则。 避免重复匹配重复定义,关注可继承属性。
Dom 离线化
所谓的 Dom 离线化就是将要操作的元素从文档流中脱离,然后再恢复它。离线的 DOM 不属于当前 DOM 树中的任何一部分,这也就意味着我们对离线 DOM 处理就不会引起页面的回流与重绘。可以使用**display: none
,上面我们说到了 (display: none
) 将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分,之后在该 DOM 上的操作不会触发回流与重绘,操作完之后再将display
**属性改为显示,只会触发这一次回流与重绘。
SSR
【长文慎入】一文吃透 React SSR 服务端渲染和同构原理[6]
在 SPA 模式下,所有的数据请求和 Dom 渲染都在浏览器端完成,所以当我们第一次访问页面的时候很可能会存在“白屏”等待,而服务端渲染所有数据请求和 html 内容已在服务端处理完成,浏览器收到的是完整的 html 内容,可以更快的看到渲染内容,在服务端完成数据请求肯定是要比在浏览器端效率要高的多。
SSR 对 SEO 是相对友好的,有些网站的流量来源主要还是靠搜索引擎,所以网站的 SEO 还是很重要的,而 SPA 模式对搜索引擎不够友好,要想彻底解决这个问题只能采用服务端直出。
当然,SSR 也会带了很多额外的工作量,而且会很大程度上增加项目的复杂度,这里需要做一个工作量与优化之间的权衡~
防抖 & 截流
理解 JS 的节流、防抖及使用场景[7]
防抖:防止抖动,单位时间内事件触发会被重置,避免事件被误伤触发多次。代码实现重在清零 clearTimeout。防抖可以比作等电梯,只要有一个人进来,就需要再等一会儿。业务场景有避免登录按钮多次点击的重复提交。
节流:控制流量,单位时间内事件只能触发一次,与服务器端的限流 (Rate Limit) 类似。代码实现重在开锁关锁 timer=timeout; timer=null。节流可以比作过红绿灯,每等一个红灯时间就可以过一批。
Web Worker
浅谈 HTML5 Web Worker[8]
Web Worker 是 HTML5 标准的一部分,这一规范定义了一套 API,它允许一段 JavaScript 程序运行在主线程之外的另外一个线程中。可以加载一个 JS 进行大量的复杂计算而不挂起主进程,并通过 postMessage,onmessage 进行通信,解决了大量计算对 UI 渲染的阻塞问题。
打包层面
图片使用 CDN
图片资源是每个项目无法绕开的,在项目中,图片资源往往是占打包体积比例较大的,并且图片资源的压缩效率也不是特别理想,所以为减少项目最后的打包体积,可以将图片上传至 CDN ,通过动态加载的方式引入图片,这样就可以避免图片增加打包体积了。
优化 SourceMap
SourceMap 的可选值如下(+ 号越多,代表速度越快,- 号越多,代表速度越慢, o 代表中等速度 )
开发环境推荐:cheap-module-eval-source-map
生产环境推荐:cheap-module-source-map
原因如下:
cheap:源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加 cheap 的基本类型来忽略打包前后的列信息; module :不管是开发环境还是正式环境,我们都希望能定位到 bug 的源代码具体的位置,比如说某个 Vue 文件报错了,我们希望能定位到具体的 Vue 文件,因此我们也需要 module 配置; soure-map :source-map 会为每一个打包后的模块生成独立的 soucemap 文件 ,因此我们需要增加 source-map 属性; eval-source-map:eval 打包代码的速度非常快,因为它不生成 map 文件,但是可以对 eval 组合使用 eval-source-map 使用会将 map 文件以 DataURL 的形式存在打包后的 js 文件中。在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。
压缩 JS 和 CSS
如果你使用的是 webpack v5 或更高版本,是开箱机带的功能,但是你的 webpack 是 v5 以下或者希望自定义配置,那么需要安装 terser-webpack-plugin
。如果使用 webpack v4,则必须安装 terser-webpack-plugin
v4 的版本。
第三方插件、库的按需引入
我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component
,然后可以只引入需要的组件,以达到减小项目体积的目的,如 import lodash
-> import lodash/get
。还可以使用一些支持 Tree Shaking 的库,如 import lodash
-> import lodash/get
。
总结
上述列出的只是有关于前端性能优化的冰山一角,比较适合对优化手段了解较少的同学用于知识储备,有兴趣的同学可以继续阅读其他性能优化相关的文章。
下面分享几篇我阅读过的一些较全的文章,可以帮助你更深层次了解:
浏览器工作原理与实践\_浏览器\_V8 原理-极客时间[9] - 强烈推荐 🔥 前端性能优化 24 条建议(2020) - 掘金[10] 前端性能优化三部曲(加载篇)[11] 全链路前端性能优化(欢迎收藏)[12]
参考资料
还在看那些老掉牙的性能优化文章么?这些最新性能指标了解下: https://juejin.cn/post/6850037270729359367#heading-0
[2]Vue 项目性能优化 — 实践指南(网上最全 / 详细): https://juejin.cn/post/6844903913410314247#heading-22
[3]HTTP2.0 新特性: https://juejin.cn/post/6844903545532071943
[4]WebP 相对于 PNG、JPG 有什么优势?: https://www.zhihu.com/question/27201061
[5]CSS 性能优化的几个技巧: https://juejin.cn/post/7077347573740077069#heading-1
[6]【长文慎入】一文吃透 React SSR 服务端渲染和同构原理: https://juejin.cn/post/6844903943902855176
[7]理解 JS 的节流、防抖及使用场景: https://juejin.cn/post/6844903669389885453
[8]浅谈 HTML5 Web Worker: https://juejin.cn/post/6844903496550989837
[9]浏览器工作原理与实践_浏览器_V8 原理-极客时间: https://time.geekbang.org/column/intro/100033601
[10]前端性能优化 24 条建议(2020) - 掘金: https://juejin.cn/post/6892994632968306702#heading-29
[11]前端性能优化三部曲(加载篇): https://juejin.cn/post/6844903863963631623#heading-16
[12]全链路前端性能优化(欢迎收藏): https://juejin.cn/post/6911512163249029134#heading-38