Web前端性能优化总结

    技术2022-09-01  86

    目录

    Canvas性能优化实践

    动画性能优化

    DOM操作优化

    HTML和CSS性能优化

    善用防抖与节流:

    Web Worker进行复杂运算。

    善用缓存

    WebSocket、SSE、HTTP2进行服务端推送。

    优化网络

    性能分析工具


    Web前端性能优化可以从很多方面描述,从首屏加载到用户交互,从HTML到JS,从静态画面到动画,从网络获取资源到缓存等,本文选择了笔者平常会考虑到的一些性能优化角度进行了描述。

     

    Canvas性能优化实践

        1.场景:有一张大的静态背景图

    优化方式:使用CSS直接绘制一幅静态的背景图,而使用Canvas来绘制。

    优化原理:在用户交互时,避免了需要重复使用Canvas来绘制大的静态背景图。

        2.场景:有特别多的Canvas元素需要绘画,而且元素需要的刷新频率不一样(可以理解为用户交互频率或者动画更新频率)。

    优化方式:使用多个Canvas绘画多个图层,根据元素的需要的刷新频率分配到不同的图层中。

    优化原理:一些动态的元素(即频繁刷新的元素)的需要频繁刷新,而如果将静态元素与动态元素放到同一图层,则会引起不必要的更新,同时为了让动态元素不影响静态元素,会有许多复杂的且不必要的运算。

        3.场景:绘制复杂的元素。

    优化方式①: 避免频繁多次调用Canvas的绘图方法(fill、stroke、fillRect、strokeRect等)

    优化原理①: 调用绘图方法带来的开销以及上下文切换开销的性能损耗是挺大的。当绘制复杂的元素时,一般建议先用使用lineTo, moveTo方法将轨迹全部描述清楚,最后再调用以此绘图方法。

    优化方式②:将一些复杂的重复元素预先在一个离屏的Canvas上绘制好(离屏就是指不在屏幕上展示的),之后等要用到这个元素了,就从这个Canvas上将渲染好的数据拿过来,直接渲染到屏幕上。

    优化原理②:减少了重复生成一个复杂图像的过程的性能开销。

        4.场景:对Canvas元素绘画坐标的精度要求不高时(不要求小数点)

    优化方式:使用整形坐标来代替浮点型坐标。

    优化原理:在使用浮点型坐标绘制图像时,浏览器为了达到抗锯齿的效果,会有额外的性能开销。

        5.场景: 用户交互导致需要频繁刷新的时候(比如拖拽移动元素)。

    优化方式:可以采取局部更新的策略,就是只更新由于用户交互而导致更新的那部分Canvas区域。

    优化原理:减少了Canvas整张画布更新所带来的性能损耗。

     

    动画性能优化

    方式:使用window.requestAnimationFrame() 来绘制动画,而不是使用setInterval。同时记得在销毁的时候要调用一下cancelAnimationFrame()。

    原因:使用requestAnimationFrame的时候可以让动画的刷新频率跟着浏览器的屏幕刷新频率走,这样就不容易引起丢帧和卡顿现象。同时requestAnimationFrame还可以有CPU节能的功能,当页面未激活的时候,由于该页面显示上不会跟着屏幕定时刷新,因此也不会触发动画,可以进行CPU节能。最后  requestAnimationFrame 还有函数节流的功能,即每一段时间内,会且仅会触发一次。

     

    DOM操作优化

    如上图所示,当在Javascript中进行DOM操作之后,浏览器的处理会依次经历后续4个阶段。Style阶段就是DOM操作会导致浏览器重新计算Style。之后如果布局发生变化了,还需要重新计算渲染树上的各个节点应该再屏幕的渲染坐标,这个阶段叫重排。然后如果渲染树上的节点的渲染数据变了(比如颜色、长宽等),还需要重新调用渲染树节点的paint方法,这个阶段叫重绘。最后就是合成阶段(Composite),这个阶段主要是将渲染树渲染出来的不同图层的数据根据层级关系,通过GPU加速合成,然后将合成的数据渲染到屏幕上。(这里渲染树为什么要进行分层呢?可以联想到上面说的Canvas性能优化时进行分层的原因。)

    频繁的DOM操作所可能带来的重排和重绘是需要大量性能的。重排一定会导致重绘,重绘不一定需要重排。所以我们的策略是尽量减少重排,甚至减少重绘。有以下几点建议:

    使用Virtual DOM,基本上主流的前端页面构建框架都支持Virtual DOM。Virtual DOM是框架内置的针对DOM操作进行优化的一套机制。基本原理是:先将一段时间内的针对实际DOM的操作作用到Virtual DOM上,然后这段时间结束后,再将Virtual DOM与实际DOM的差别使用Diff算法计算出来, 之后将差别的部分更新到实际DOM。好处就是在频繁的DOM操作时,减少了许多无用的重排和重绘的计算开销。

    框架不支持Virtual DOM时可以考虑:需要对DOM进行大量操作时,不要直接操作DOM,而是先使用DocumentFragment,将DOM操作依次映射到DocumentFragment上,最后再将DocumentFragment映射到真实的DOM上。

    如果不使用DocumentFragment,则可以先把需要修改的部分DOM设为display:none 或者position: absolute、 position: fixed,在修改完毕再将修改好的部分DOM给改回来。这样可以减少重排。

    尽量使用不会导致重绘和重排的DOM操作:使用transform以及 opacity 属性修改样式的时候,不会触发重排和重绘,浏览器会直接进入合成阶段,使用之前计算的好的图像数据进行矩阵变换,最后将图像数据渲染到浏览器上。这个过程还可以用到GPU进行硬件加速。

     

    HTML和CSS性能优化

    上图是浏览器解析HTML和CSS文件并且构建渲染树,将画面渲染到屏幕上的原理流程图。

    原理图中有个HTML Parser和 CSS Parser。HTML Parser 主要分为两个阶段,一个是标记化算法以及树构建算法。由于最终需要构建一个DOM树,所以在解析构建树的时候嵌套的HTML标签层级越深,构建时所需要的性能开销越大。CSS Parser解析的时候,CSS选择器本身就是一个个的嵌套的树结构,解析的时候是右向左解析,这样可以增加一部分解析效率,但是如果CSS选择器嵌套的层次过深,CSS解析时的性能开销也会比较大。结论:减少不必要的HTML标签层级嵌套以及CSS选择器层级嵌套。

    有一点需要注意,如果要引入外部脚本的话,由于浏览器遇到普通<script>标签的时候会立即读取和运行脚本,由于Javascript是单线程的,因此页面的渲染就会被阻塞。为了避免这种情况。可以采取以下策略:

    将所有的<script>标签置于HTML文档的底部。

    给<script>标签添加defer或者async属性。添加了这两个属性的脚本都不会阻塞页面的解析和渲染。但是他们略有区别:defer属性是立即加载脚本,但是延时到页面渲染完才执行,async属性是使用其他线程立即加载脚本并且加载完立即执行。defer标签可以保证脚本执行顺序,而async标签不能保证。

     

    善用防抖与节流:

    lodash库中就有debounce(防抖)、throttle(节流)方法。

    防抖的作用:在事件触发后的一段时间内,该事件不再触发,才调用该事件预设的回调函数,若该事件持续触发,则只执行最后满足条件那次回调函数。这样避免了过于频繁地调用回调函数,而且如果事件持续触发,其实前面所调用的回调函数的计算结果有时都是无用的。

    防抖的常见适用场景:自动根据用户输入搜索内容等只需要用户持续交互完成后才进行一次回调函数调用的场景。

    节流的作用:在事件触发后的一段时间内,会且仅会执行一次该事件的回调函数。同样可以避免过于频繁地调用回调函数。

    节流的常见适用场景:拖拽视频的进度条、改变窗口大小时的自适应、使用滚轮来展开页面底部的被折叠的内容等。

    防抖与节流的实现原理其实很简单,前者就是主要就是利用闭包保存了上一次需要被触发的回调函数的参数。后者就是利用闭包保存了上一次已经触发回调函数的时间,之后当事件触发后就对比当前时间与上一次触发时间的时间差。若不想引入较大的第三方库,也可以考虑自己实现。

     

    Web Worker进行复杂运算。

    Javascript设计之初就是单线程的,所有的代码都在主线程的调用栈中执行,而调用栈中的代码的入栈出栈则由事件循环控制。事件循环机制使得单线程的Javascript能较好地处理事件,但是单线程使得Javascript在处理复杂运算的时候就会占用主线程,从而导致界面交互的阻塞。

    Web Worker就是用来解决这个问题的,它可以在主线程之外,再开一个独立的子线程,这个子线程是一个全新的运行环境,有自己的全局变量,同时不可操作DOM、BOM。主线程与子线程通过消息来传递数据。通过让子线程进行复杂运算,运算结束后将运算结果传递给主线程,这样就可以避免在进行复杂运算时,用户交互被阻塞,避免用户交互卡顿等现象。

     

    善用缓存

    前端缓存可以分为很多种:CDN,HTTP缓存,浏览器本地存储,Service Worker。为什么使用缓存可以提高性能呢?使用缓存首先可以减少前端获取新资源的等待时间, 使页面交互和加载更流畅。其次缓存可以减少服务器对于请求的响应压力。

    CDN:就是内容分发网络,可以理解为在靠近用户的网络节点建立一个个缓存服务器。 主要用到了负载均衡、动态分发与复制、http缓存等技术。

    浏览器本地存储:可以分为cookie、localStorage、sessionStorage、indexDB。从存储 容量、有效期、存储信息格式上来说,各有特点,不再赘述。

    HTTP缓存:都是当客户端向服务器发起第二次以上的重复get请求的时候才会生效。分为强制缓存与协商缓存,区分标准就是有没有与服务器进行通信。

    强制缓存:第一次get请求时,服务器返回的响应头中可以带上Cache-Control, Progma, Expires等字段。用来标识该资源是否启动强制缓存,以及缓存资源的有效期。一般使用Cache-Control: max-age比较多,表示资源在多久后过期,而expires字段的值是一个UTC时间点,如果在服务器时间与客户端时间不一致的情况下则会不太可靠。浏览器启动HTTP缓存的时候,会先判断是否命中强制缓存,如果命中了则不需要向服务器发送请求,直接返回强制缓存中的资源,然后状态码为200.

    协商缓存:当强制缓存不生效时,浏览器才会去使用协商缓存。协商缓存主要用到两对请求头和响应头。一对是响应头Etag 和 请求头If-None-Match. 另外一对是响应头Last-Modified 和请求头If-Modified-Since。其中Etag和If-None-Match的值都是一个服务端资源hash运算后的key。而Last-Modified和If-Modified-Since则是一个UTC时间点,代表资源最后更新的时间。这些值会在第一次Get请求的时候由服务器放在响应头里。然后浏览器执行协商缓存的时候再把这些值放到请求头字段中去。

    由于Last-Modified是用来标识资源更新的最后时间点,而且标记粒度只到秒级,所以不太可靠。一般如果If-None-Match和If-Modified-Since请求头同时存在的话,If-Modified-Since会被浏览器忽略。服务端接收到代表协商缓存的请求头字段后,就对本地资源进行对比,若资源没更新,则直接返回状态码304,不需要传送资源。若资源已经变动,则返回200,然后传送新的资源以及新的Http缓存响应头字段。

    Service Worker:Service Worker可以理解为是对Web Worker的进一步的封装,特化 成一个专门用来处理网络请求的本地中介(Proxy)。可以用它来进行网络请求的缓存以及 转发。使用Service Worker来进行缓存,则是在Javascript中直接拦截浏览器http请求, 并设置缓存的文件,直接返回,不用经过服务器。在一些PWA(Progressive Web App) 中,经常使用Service Worker来做一些离线的应用,以增强Web APP的能力,由于它 是在另外一个线程运行的,因此不会影响主线程的运行。

     

    WebSocket、SSE、HTTP2进行服务端推送。

    前端更新数据的一般策略就是使用轮询调用AJAX请求,但是这样会有比较大的性能开销:

    ①可能在服务器数据未更新的时候,前端也不停地发送请求,重复获取同样的数据。

    ②如果需要轮询的请求比较多,而且轮询间隔也不一致,那么就会需要定义很多定时器。

    为了解决前端轮询更新数据的性能问题,推荐使用服务端推送。

    WebSocket:webSocket是一种不同于HTTP的应用层协议,能够提供给客户端和服务端全双工通信的能力。服务端可以通过事件触发,主动向客户端推送数据。客户端也可以不用发送HTTP请求,直接跟服务端进行通信。WebSocket性能开销比SSE和HTTP2的方式要大,主要用在需要全双工通信的场景,比如即时通讯、直播、网络游戏等。

    SSE:即服务端推送事件(Server Send Event)。本质上是利用Http1.1以上版本的长连接功能进行服务端事件的推送。比WebSocket轻量,只能从服务端推送数据到客户端,支持断线重连,发送的是流信息。

    HTTP2:HTTP2与HTTP1.X最大的区别就是对性能进行了极大的优化。具体来说,变 化点有:

    ①采用二进制数据的方式传送信息

    ②只发送头字段的索引,对头字段进行了压缩

    ③支持基于TCP的多路复用,并发请求和响应,通过帧ID识别请求。

    ④支持服务端推 送数据到客户端。

    但是得注意HTTP2的服务端推送数据到浏览器的话,在JS中是感知不到的,推送的数 据是保存在浏览器中,由浏览器默认处理为HTTP缓存。之后在JS中请求该数据时就 可以不经过服务端。

     

    优化网络

    在TCP/IP协议的网络模型中,计算机网络自下而上分为四层:链路层、网络层、传输层、应用层。对于web开发人员来说,在开发中可以更改的一般就是传输层与应用层。这两层主要用到了TCP协议(传输层)、HTTP协议(应用层)。因此可以从这两层协议的角度来对网络进行以下优化。

    优化TCP的拥塞控制算法

    传统的TCP拥塞控制算法用的是RENO算法,是基于丢包的拥塞控制算法。该算法基于四个阶段:①慢启动 ② 拥塞避免 ③快重传 ④快恢复。由于RENO是基于丢包的拥塞控制算法,因此一旦出现丢包,就会进入拥塞避免阶段,传输窗口的大小立马减半,这样其实不利于充分利用网络带宽。

    优化:可以使用BBR算法来替代RENO。BBR(基于拥塞的拥塞控制算法),不把丢包作为网络拥塞的判断依据,而是当网络上的数据包的总量大于网络瓶颈链路带宽和时延的乘积的时候,才判断为网络拥塞。除了判断网络拥塞的条件不一致以外,BBR与RENO的其他阶段基本一致。BBR适用于高时延、高带宽、有一定丢包率的长肥网络,可以有效地降低传输时延,并且提高吞吐量。

    从HTTP1.X 升级到 HTTP2

    HTTP2与HTTP1.X最大的区别就是对性能进行了极大的优化。具体的变化点在上一节已经说到,不再赘述。HTTP2的TCP多路复用与HTTP1.X的长连接的TCP复用不同。HTTP2中,不同的请求与响应可以并发。而在HTTP1.X的长连接的TCP复用中,请求与响应是串行的,在一个TCP连接中,一个请求的响应结束后,才可以发送下一个请求。

     

    性能分析工具

    chrome自带的任务管理器,可以查看各个标签页的一些性能指标。

    chrome的开发者工具中,Memory项可以截图某几个时间点的内存指标快照,然后进行内存的对比,也可以记录一段时间内的内存变化,然后进行内存的分析。

    chrome的开发者工具中,Performance项可以查看一段时间内APP的各种性能运行指标。一般用的比较多的是分析首屏加载过程。Performance中主要分为以下几项:

    Network: 使用时间轴的方式记录下在某一段时间内的请求的耗时以及发起和结束的时间节点。Frame: 记录下在每个时间节点时,界面渲染出来的画面是怎么样的。Timings: 在时间轴上标识出画面渲染的关键性能指标的完成时间点,比如FCP(首次内容渲染)、FMP(首次有意义内容的渲染)、 DCL(DOMcontentLoaded)等。Main:又称火焰图,可以详细地看到具体每个函数的执行节点以及执行时长。执行时长过长的将会被标识出来。同时选中火焰图选项后,在屏幕中部有一段会从不同的维度显示这段时长内的内存变化曲线图。

    在Performance页的首部有一段整体的时间轴控制区,可以通过滚轮放大缩小选中的区域大小,同时在选中区域的时间轴上拖拽,可以移动选中区域的时间窗。

    4.JsPerf:一个在线的代码片段性能测试工具,可以对代码片段进行性能分析,并且输出对比结果。结果一般是以ops/sec 为指标输出的,代表每秒钟可执行代码段的次数。次数越多性能越好。

     

    推荐书目:Web性能权威指南

    Processed: 0.012, SQL: 9