前端性能优化总结与分析

 
notion image
前端性能优化可以从以下几方面入手。
1. 网络优化 2. 页面解析和渲染 3. 运行时优化 4. 用户感知体验优化

网络加载时

减少HTTP请求(节流)
采用HTTP2协议替代HTTP1.1
静态资源使用CND加速
服务器采用gzip等压缩算法压缩资源
tree-shaking与代码压缩
图片压缩和图片懒加载
代码分割和懒加载
preload和prefetch
合理地使用缓存策略HTTP缓存本地存储

解析渲染时

css加载使用link标签来替代@import
script标签使用deferasync来避免js阻塞渲染
preload避免css阻塞,此外还可以加载大体积资源
降低css选择器的复杂性、优化DOM结构
SSR替代SPA(服务器渲染替代单页面应用)
优先使用弹性盒子布局或网格布局
优先使用css动画而不是js动画

运行时

减少回流和重绘
防抖和节流
使用新DOM API querySelector
事件代理
避免无限微任务和宏任务长时间执行
处理大量数据——分页和长列表优化
动画尽量使用transformfilteropacity等属性触发硬件加速
requestAnimationFrame来替代定时器动画
使用overflow-anchor减少页面偏移(默认启动)

用户感知体验优化

首屏加载策略
加载动画
骨架片
 

分析

减少HTTP请求

减少HTTP请求主要有两方面考量,一方面因为目前主流的HTTP1.1存在队头阻塞(请求后必须要得到响应才能发送下一个请求)问题,并且浏览器一般最多同时支持6个HTTP请求,所以应该减少对HTTP资源的占用;第二就是为了能够减轻服务器的压力,例如一个提交按钮,点一次就发送一次HTTP请求,在高峰时期服务器资源可能比较紧张,这种情况下如果能够减少重复无用的HTTP请求能缓解服务器压力。
对于第一点来说,可以通过雪碧图、图片懒加载等方式来解决,但是我认为最好的方式还应该是使用HTTP/2。对于第二点,可以运用防抖节流技术、本地存储数据等方式来解决。

采用HTTP2协议替代HTTP1.1

对比HTTP1.1,HTTP/2做了很多改进。
首部压缩,压缩首部内容
二进制协议,HTTP1.1是文本的,而HTTP2是基于二进制的协议,文本协议对人友好,而二进制协议对计算机更友好。
多路复用,通过二进制分帧层,将HTTP请求分为一个个帧,发送给服务器,服务器接收后将帧重组为HTTP请求。
服务器推送,服务器能主动推送与接收到的HTTP请求相关的资源
优先级,HTTP2可以设置每个流的优先级。
相比HTTP1.1,HTTP2提高了传输效率,尤其是多路复用功能解决了队头阻塞问题。HTTP2通过二进制分帧层将HTTP请求封装成一个个帧,一个请求表示一个流,HTTP2支持发送多条流,这意味浏览器可以同时发送多个HTTP请求,不需要得到响应才发送下一个请求,而服务器接收后可以通过帧id来识别此帧是那条流,从而重组成HTTP请求。

静态资源使用CDN加速

使用CDN能够实现资源的加速访问。CDN加速的原理是CDN广泛地采用各种缓存服务器,将这些缓存服务器部署到用户访问相对较近的地区中,当用户访问网站时,利用全局负载技术根据用户所在的地区、网络运营商、各节点的负载情况等信息将用户的访问指向最优的缓存服务器上,克服了传统访问因跨运营商、跨地区导致访问慢的问题,同时也能缓解源站点服务器压力。
不使用CDN访问网站过程:
1. 1.用户主机向本地域名服务器发起域名解析请求 2. 2.本地域名服务器向根域名服务器查询 3. 3.本地域名服务器向顶级域名服务器查询 4. 4.本地域名服务器向二级权限域名服务器查询 5. 5....... 6. 6.本地域名服务器查询到结果并返回给主机
使用CDN时访问网站流程:
1. 1.用户主机经本地域名系统(DNS)解析,向CDN的DNS调度系统发送解析请求 2. 2.DNS调度系统将用户响应速度最快的缓存服务器的ip地址返回给用户 3. 3.用户向该缓存服务器发送请求 4. 4.缓存服务器返回结果,如果缓存服务器之前没有缓存该内容就会向原站点服务器请求并缓存。

资源压缩

服务器将资源通过gzip等算法压缩后再发送给浏览器,资源压缩率越高,体积越小,加载速度就越快。
浏览器再发送http请求时会通过Accept-Encoding标识浏览器支持的压缩算法,响应头通过Content-encoding来标识资源所使用的算法。

图片压缩和图片懒加载

在一些场景下会加载大量的图片,这种情况下对图片优化可以显著加快网站加载速度。
网站的图片有时会有略缩图和大图两种状态,当加载略缩图时请求压缩过的图片,当用户点击图片查看大图时加载展示原图。加载略缩图的情况比较常见,可以使用压缩后的图片以加快加载速度,即使图片质量比原图低,也不会造成不好的用户体验,而用户查看大图的概率较低,并且图片质量比加载速度更重要,因此应当加载原图。
除了图片压缩外可以利用图片懒加载技术,优先加载出现在用户屏幕中的图片。
图片懒加载实现方式有两种,一种是监听页面滚动,根据图片的几何属性和页面滚动量判断图片是否在用户屏幕上,另一种是利用新api IntersectionObserver

合理地使用缓存策略

缓存策略可以分为两块,一块是HTTP缓存,另外一块是本地缓存。
HTTP缓存
第一次加载资源后如果资源标识可以缓存,那么浏览器会将其缓存下来,在缓存过期前再次加载此资源就会直接使用本地缓存而不是发送HTTP请求,这是强缓存,当缓存资源过期后再次加载此资源,会使用协商缓存,就是将资源的标识等信息发送给服务器,服务器判断资源是否有更新,若有则返回更新后的资源,若没有更新则返回状态码304并重新设置过期时间。
从上面可以看出强缓存能够减少HTTP请求,减少加载资源的时间,但是缺点是无法保证资源的实时性,协商缓存能够保证资源的实时性,但是加载资源速度会比强缓存慢。因此可以根据项目中的实际情况合理地设置HTTP缓存过期时间,对于实时性要求不高,但是对加载速度有要求的资源,例如图片,可以适当地延长HTTP缓存过期时间,对于一些实时性要求高的资源则缩短HTTP缓存过期时间,如果对于两者都有要求的话,可以用cache-control:no-cache
HTTP的资源是否更新服务器会通过etaglast-modified来判断,etag是根据资源内容生成的,如果资源更新了,那么资源的etag也会改变,服务器通过比较可以判断资源是否有更新,last-modified表示资源最后修改时间,服务器通过此时间来判断资源是否已经是最新的。因为last-modified时间单位是秒,而etag可以更精确,因此etag的优先级更高。
etaglast-modified是响应头的名称,在请求头对应的名称是If-None-MatchIf-Modified-Since
HTTP缓存时间可以通过ExpiresCache-Control(更常见)来控制,
(1) max-age:用来设置资源(representations)可以被缓存多长时间,单位为秒;
(2) s-maxage:和max-age是一样的,不过它只针对代理服务器缓存而言;
(3)public:指示响应可被任何缓存区缓存;
(4)private:只能针对个人用户,而不能被代理服务器缓存;
(5)no-cache:强制客户端直接向服务器发送请求,也就是说每次请求都必须向服务器发送。实际上资源仍然会被缓存,但是会通过询问服务器来判断使用本地缓存还是获取服务器的资源
(6)no-store:禁止一切缓存(这个才是响应不被缓存的意思)。
 
本地存储
合理地使用本地存储也能加快网站访问时间,在浏览器中常用本地存储有SessionStorageLocalStorage,两者的区别是一个是会话存储,当网站标签页或浏览器关闭后存储被清空,另一个是长期存储,除非手动清理,否则不会被自动清除。在小程序中也有Storage的概念,与浏览器的localStorage非常相似。
为什么不说cookie?事实上cookie也能存放数据,但是它会被HTTP请求携带,影响速度,因此将本地数据存放到cookie是一个非常糟糕的决定。
本地存储可以做到类似cache-control:no-cache的效果。
const storageDate = { cacheTime: new Date().getTime(), data: // 要存储的内容 } // 后面加载存储时验证一下时间,时间长了就重新发送请求

css加载使用link标签来替代@import

使用这条优化策略的原因是@import是串行加载,而link标签是并行加载。@import只有在加载完上一个css文件后才去加载下一个css文件,而link标签会同时加载所有通过link标签引入的css文件,因此通过link标签加载css文件的速度快于通过@import加载。

script标签使用deferasync来避免js阻塞渲染

当浏览器遇到<script>标签后就会停止解析,转去下载js文件并执行,这个过程阻塞了HTML的解析,我们可以用deferasync来避免js阻塞渲染。
defer script: 浏览器会发送http请求加载js文件,但是不会阻塞HTML的解析,等到HTML解析完毕后才去执行js代码。
async script:浏览器会发送http请求加载js文件,但是不会阻塞HTML的解析,但是当下载完后会立刻执行,这时会阻塞HTML的解析,由于此时不能保证DOM已经构建完毕,因此最好不要此时执行DOM操作。

降低css选择器的复杂性、优化DOM结构

css选择器的匹配规则是从右到左匹配,先从叶子节点开始,然后向上匹配。如果能更快地匹配到元素,那么就能缩短解析时间,因此应该降低css选择器的复杂性。
此外一个页面中的DOM节点不应该太多,也尽量不要嵌套太深,否则会页面性能会有影响,尤其是长列表,如果不优化很容易造成节点数量太多从而导致页面卡顿。

SSR替代SPA(服务器渲染替代单页面应用)

SPA(即单页面应用)顾名思义只有一个页面,所谓的页面跳转其实是通过DOM API重构了页面,让用户看起来好像真的跳转到另一个页面了一样。
SPA有以下缺点
  1. 首次加载慢,因为首次加载要加载大量的资源,不过可以利用代码分离和懒加载优化。
  1. 不利于SEO,因为页面内容是网络请求数据后通过DOM动态构建而成。
优点是
  1. 切换页面较快
  1. 跳转页面不会发起页面请求,而是通过DOM重构页面,减轻了服务器压力
  1. 便于前后端分离,提高开发效率
  1. SSR(即服务器渲染)与SPA的区别是服务器渲染会提前就将HTML处理好,浏览器只要请求页面直接渲染就行了。
SSR的优缺点
  1. 首次加载较快
  1. 利于SEO
  1. 不利于前后端分离模式
  1. 占用服务器资源

优先使用弹性盒子布局或网格布局

浮动布局主要的优点是兼容性好,能够支持IE浏览器,但是缺点是不易维护、性能比较差,毕竟float最开始设计目的只是为了实现文字环绕图片的效果,因此除非要兼容远古浏览器,不然还是用弹性盒子布局或网格布局,弹性盒子布局+网格布局+定位基本上能满足所有布局要求。

优先使用css动画而不是js动画

优先使用css动画有如下理由:
1. 1.js由主线程执行,主线程还会执行其他任务,可能会造成阻塞,这会影响动画流畅度。 2. 2.CSS动画是运行在合成线程中的,不会阻塞主线程,并且在合成线程中完成的动作不会触发回流和重绘。
虽然css动画的性能比js动画性能好,但是js动画能做到更精细地控制,此外新apirequestAnimationFrame性能比过去的定时器性能更好。当要制作复杂的动画效果时,还是应该采用js动画,而制作简单动画效果时还是应该首先考虑使用css动画。

动画尽量使用transformfilteropacity等属性触发硬件加速

当使用transformfilteropacity等属性时,浏览器会将元素提升为独自的一层,并使用GPU加速(硬件加速),因为元素位于独自的一层,因此变化的过程中不会影响到其他图层,因此使用transformopacity等的性能会更好。
尤其是在做动画的时候,transform: tanslateposition + left这种方式性能更好,在对性能要求很高的小程序上,建议优先使用transform,但是这里有个坑,如果祖先元素有transform属性会导致元素position:fixed的行为变为position:absolute

减少回流和重绘

页面渲染主要会经过5个阶段。
1. 1.主线程解析HTML生成DOM 2. 2.主线程解析CSS生成CSSOM,确定哪些样式作用在哪些属性上。 3. 3.布局。将DOM和CSSOM合成渲染树,计算每个节点的几何属性和样式,一旦有元素被移动或大小被改变,就会触发回流,重新计算布局。 4. 4.绘制。这个阶段主要是填充像素,比如描绘文本,着色图片、添加边框等操作就是在这个阶段完成的,这不会直接显示在屏幕上,而是在内存中使用CPU绘制,页面被分隔为多个图层。如果元素样式变动,就会触发重绘,重新绘制。 5. 5.合成。这个阶段浏览器收集所有绘制完成的图层,将他们分别光栅化,最后在合成线程里合并为一个页面显示在屏幕上。为了找出哪些元素需要在哪些层中,主线程会遍历渲染树来生成层树。
当有元素几何属性改变时,会触发回流重新计数布局,并且回流还会导致触发重绘,当元素样式改变但是几何属性没有改变时,只会触发重绘,因此减少回流和重绘的次数对前端性能极为重要,尤其是回流。
减少回流和重绘的一些方法
修改className而不是一条条地修改样式。如果要修改多条DOM样式,更好的办法是预先定义好class,然后修改className
将DOM离线(display:none)后再修改,虽然离线时和恢复时会导致回流,但是离线后对元素的修改不会导致回流。
尽可能的修改层级比较低的DOM
为动画的元素使用fixed或absoult的定位,因为浏览器会将这种元素单独提升自一层绘制,不会影响到其他图层。
尽量使用transformopacity等属性做动画,浏览器会将这类元素单独提升自一层绘制,此外还会使用GPU绘制。

防抖和节流

函数防抖:事件触发后,n秒后再执行回调,如果在这段时间里再次触发事件,则重新计时。
函数节流:事件触发后立刻执行回调,n秒内再次触发事件不会再执行。
函数节流和函数防抖对前端性能优化很有帮助,举几个例子:对表单的提交按钮做了节流处理能够减少发送无用的http请求,尤其是在高峰时期对缓解服务器压力有很大的帮助。
在监听页面滚动的场景中,我们只需要最后一次滚动事件中获取页面滚动量并执行某些处理,因为这些处理会进行大量计算和操作,如果不做防抖处理,那么每次滚动都会执行这些处理,这就很可能会造成页面滚动卡顿,但是如果做了防抖处理,那么就能大大减少执行处理的次数。

使用新DOM API querySelector

新的DOM API querySelectorquerySelectorAll除了在写法上与传统的getElementByIdgetElementsByClassName等DOM API有区别外,最大的区别就是querySelector返回的是当时DOM节点的快照而不是实时对象。
传统中通过getElementById获取的DOM对象会一直保持最新状态,对此节点对象作出的任何更改都会反映到获取到的节点对象中,这毫无意义会对性能造成影响,因此JavaScript新APIquerySelector一方面支持css选择器写法来获取节点对象,另一方面是只返回当时节点对象的快照,不会实时更新,从而节省资源开销。

路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。路由懒加载 | Vue Router (vuejs.org)
利用webpack代码分离功能实现路由懒加载。
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue') const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue') const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
 

requestAnimationFrame来替代定时器动画

requestAnimationFrame是HTML5新增加的API,类似于定时器,requestAnimationFrame按帧对网页进行重绘(浏览器的渲染页面的标准帧率为 60FPS),并由系统来决定回调函数的执行时机
传统的使用定时器来实现动画在低端机上很容易导致卡顿、抖动问题,这主要是两个原因:1. 定时器的执行时间不是固定的(原因参考事件循环),2. 不同设备的屏幕刷新率可能不同,setTimeout只能设置固定的时间间隔。
而使用 requestAnimationFrame 执行动画,最大优势是能保证回调函数在屏幕每一次刷新间隔中只被执行一次,这样就不会引起丢帧,动画也就不会卡顿,性能比定时器要好。

事件代理

<div id="div"> <button class="button">按钮</button> <button class="button">按钮</button> <button class="button">按钮</button> <button class="button">按钮</button> </div>
对于以上这种场景,如果要监听每个按钮点击事件的话,相比较为每个按钮添加事件处理程序,更好的方式是为他们的公共父节点div添加事件处理程序,由于事件传播机制,点击button也会触发div的点击事件处理程序。这样做的好处是不用创建大量的事件监听器,节省开销。
此外有时候我们还需要知道点击的是哪个按钮,这时候可以为这些按钮添加自定义属性data-属性名,然后通过dataset对象来判断触发事件的是哪个按钮。
<body> <div id="div"> <button class="button" data-index="1">按钮</button> <button class="button" data-index="2">按钮</button> <button class="button" data-index="3">按钮</button> <button class="button" data-index="4">按钮</button> </div> </body> <script> let div = document.querySelector("#div"); div.addEventListener("click", (e) => { if (e.target.dataset.index === 1) { // 第一个按钮被点击 } }); </script>

处理大量数据——分页和长列表优化

当有大量数据需要展示的时候,最好不要一次性将所有数据都加载出来,这样容易造成页面卡顿,在移动端甚至会导致内存问题。最好采用分页的形式,一页一页的加载数据。
分页能够避免一次性加载太多数据所导致的页面卡顿问题,但是无法避免节点积累所带来的内存问题。最好结合虚拟列表技术,将用户屏幕不可见的组件释放,从而避免内存累积导致页面卡顿的问题。
长列表优化方案可以参考我的另一篇文章

避免JS占用时间过长

事件循环机制步骤:
1. 1.将同步代码放到执行栈中执行 2. 2.执行栈空闲时检查微任务队列,取出所有微任务并执行 3. 3.检查宏任务队列,取出一个宏任务执行 4. 4.检查需要重新渲染页面 5. 5.重复以上操作
如果执行js计算量大,操作复杂,需要很长的时间,最好是能将其拆分几部分到宏任务中,实现方式是通过定时器回调,从而避免js执行时间过长导致浏览器无法重绘界面,导致页面卡顿,一般计算机的刷新率是60Hz,大约16ms左右重绘一次。

首屏加载策略

首屏加载策略其实就是让最先出现在用户面前的部分尽快地加载出来。首屏加载策略的关键在于关键css
关键css,即折叠之上(网站顶部开始到屏幕底部结束的部分)的内容样式需要优先加载,可以通过内联样式来实现。
非关键css,即首屏以外的内容样式,这类样式加载优先级比关键css低,应当通过link标签加载。
首屏加载策略另一个重要的方面是要避免阻塞,一方面<script>标签所导致的渲染阻塞,可以通过前面说的deferasync解决,也可以用<script>放置文档最底部来避免阻塞。另一方面是link加载非关键css导致的阻塞问题,解决方法是使用rel="preload" as="style"来避免阻塞,声明preload的资源会在页面生命周期早期就提前加载,从而避免阻塞渲染。