前端性能优化

494 阅读14分钟

题外话:

之前解决性能优化是看到问题后快速解决,但如何使用测试工具及性能指标都没有概念。

评判网站快慢的指标有哪些?性能测试工具又有哪些?给大家分享下常见的几种性能指标,性能测试工具以及对应的请求和响应优化方案

Web性能指标-评判网站快慢

RAIL性能模型

响应(Response):应该尽可能快速的响应用户,应该在100ms以内响应用户输入。

动画(Animation):在展示动画的时候,每一帧应该以16ms进行渲染,这样可以保持动画效果的一致性,并且避免卡顿。

空闲(Idle):当使用Javascript主线程的时候,应该把任务划分到执行时间小于50ms的片段中去,这样可以释放线程以进行用户交互。

加载(Load):应该在小于1s的时间内加载完成你的网站,并可以进行用户交互。

根据网络条件和硬件不同,wifi通常在1秒内完成,如果是在移动设备3G下,加载通常在5s内。

基于用户体验的性能指标

FCP(First Contentful Paint)首次内容绘制(白屏时间),浏览器首次绘制来自DOM的内容的时间,内容必须是文本、图片(包含背景图)、非白色的canvas或SVG,也包括带有正在加载中的Web字体的文本。

C36E21FD-8002-466F-B3A9-9F482C9E604F.png

LCP(Largest Contentful Paint)最大内容绘制,可视区域中最大的内容元素呈现到屏幕上的时间,用以估算页面的主要内容对用户可见时间。最大内容未必是最后加载出现的,也有可能是阻塞后面加载的最大内容。

49EA2120-4A59-4D60-90ED-0F84229F2CAC.png

FID(First Input Delay)首次输入延迟,从用户第一次与页面交互(例如单击链接、点击按钮等)到浏览器实际能够响应该交互的时间。输入延迟是因为浏览器的主线程正忙于做其他时间,所以不能响应用户。发生这种情况的一个常见原因是浏览器正忙于解析和执行应用程序加载的大量计算的JavaScript。

44F52530-860A-4F8B-BCE9-9D4406B4A6B7.png

TTI(Time to Interactive) 完全达到可交互状态,浏览器可以持续性的响应用户的输入。最后一个长任务完成的时间,并且随后的5秒内网络和主线程师空闲的。

6FF8A149-7B3C-451F-979B-255110A259C6.png

TBT(Total Block Time)总阻塞时间,度量了FCP和TTI之间的总时间。

A915594E-1BE8-4B27-AC9C-6478F7C58DA8.png 虽然在主线程上运行任务花费的总时间未560毫秒,但只有345毫秒的时间被视为阻塞时间。

76F9DE7F-FF47-4421-96E8-B0E4DBD9C79F.png

CLS(Cumulative Layout Shift )累计布局偏移(布局抖动),CLS会测量在页面整个生命周期中发生的每个意外的布局移位的所有单独布局移位分数的综合,它是一种保证页面的视觉稳定性从而提升用户体验的指标方案。

7DBF08D9-A6BC-43E7-ABE9-18C180FDD41B.png

Web Vitals

降低学习成本,提供了一组统一的质量衡量指标 - Core Web Vitals,其中包括加载体验、交互性和页面内容的视觉稳定性。

三个指标:LCP/FID/CLS

Web性能测试 - 衡量快慢的表现方式

灯塔Lighthouse

地址:github.com/GoogleChrom…

由Google开发开源的一个web性能测试工具。通过监控和检测网站应用的各方面性能表现,来为开发者提供优化用户体验和网站性能的指导建议。

WebPageTest

地址:网页测试 - 网站性能和优化测试 (webpagetest.org) 

可部署到本地,及测试不同地区访问。

Chrome DevTools

网络请求阻止 Ctrl+Shift+P -> Show Network Request Blocking,启动网络请求阻止,添加阻止规则

Coverage 面板,查看资源使用情况

Memory 面板,查看内存问题,预测内存泄漏问题

Performance 面板,网站运行期间性能表现

请求和响应优化

DNS解析

一般来说,在前端优化中与DNS有关的两点:

  1. 减少DNS的请求次数。资源划分为至少两个但不超过四个域名,这将在减少DNS查找和允许高度并行下载之间取得良好的折衷。
  2. 进行DNS预获取:DNS Prefetch。 提前解析DNS,不会造成阻塞。

dns-prefetch注意事项

  1. dns-prefetch 仅对跨域域上的DNS查找有效,避免使用它来指向当前站点或域。到浏览器看到提示时,站点域背后的IP已经被解析。
  2. 需慎用,多页面重复DNS预解析会增加重复DNS查询次数
  3. 默认情况下浏览器会对页面中和当前域名(正在浏览网页的域名)不在同一个域的域名进行预获取,并且缓存结构,这就是隐式的DNS Prefetch。如果想对页面中没有出现的域进行预获取,那么就要使用显示DNS Prefetch。
  4. 虽然使用DNS Prefetch能够加快页面的解析速度,但也不能滥用,有开发者指出禁用DNS预读取能节省每月100亿的DNS查询。

更多DNS解析优化

  1. 延长DNS缓存时间
  2. 尽可能使用A或AAAA记录代替CNAME
  3. 使用CDN加速域名
  4. 自己搭建DNS服务

HTTP 长连接

HTTP1.1以后自动实现长连接也叫持久连接(HTTP Persistent Connections),即TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive。

管道机制

HTTP1.1版本引入了管道机制(pipelining),即在同一个TCP连接里面,客户端可以同时发送多个请求。管道化技术出现后,不用等待响应即可直接发送下一个请求。这样就能够做到同时并行发送多个请求,而不需要一个接一个的等待响应了,与挨个连接相比,用持久连接可以让请求更快结束。而管道化技术则比持久连接还要快。请求数越多,时间差就越明显。

举例来说,客户端需要请求两个资源。以前的做法是,在同一个TCP连接里面,先发送A请求,然后等待服务器作出回应,收到后再发出B请求。管道机制则是允许浏览器同时发出A请求和B请求,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求。

Content-Length

一个TCP连接现在可以传送多个回应,势必就要有一种机制,区分数据包是属于哪一个回应的。这就是Content-Length字段的作用,声明本次回应的数据长度。

3375E68F-36AC-4543-B94D-4C4381343D49.png 上面代码告诉浏览器,本次回应的长度是3495个节点,后面的字节就属于下一个回应了。

分块传输编码

使用Content-Length字段的前提条件是,服务器发送回应之前,必须知道回应的数据长度。

对于一些很耗时的动态操作来说,这意味着,服务器要等到所有操作完成,才能发送数据,显然这样的效率不高。更好的处理方式是,产生一块数据,就发送一块,采用“流模式”(stream)取代”缓存模式”(buffer)。

因此,1.1版本规定可以不使用content-length字段,而使用“分块传输编码”(chunked transfer encoding)。只要请求或回应的头信息有Transfer-Encoding字段,就表明回应将由数量未定的数据块组成。

72C47AE1-9CD5-4E7A-99AB-B40727488591.png

每个非空的数据块之前,会有一个16进制的数值,表示这个块的长度。最后是一个大小为0的块,就表示本次回应的数据发送完了。

长连接的缺点

所有的数据通信是按次序进行的,服务器只有处理完一个回应,才会进行下一个回应。要是前面的回应特别慢,后面就会有许多请求排队等着。这称为“队头堵塞”(Head-of-line blocking)。

为了避免这个问题,只有两种方法:

  1. 减少请求数
  2. 同时多开持久连接

这导致了很多的网页优化技巧,比如合并脚本和样式表、将图片嵌入CSS代码、域名分片(domain sharding)等等。如果HTTP协议设计得更好一些,这些额外的工作是可以避免的。

HTTP 2

2009年,自行研发SPDY协议,主要解决HTTP1.1效率不高的问题。

这个协议在Chrome浏览器上证明可行以后,就被当作HTTP/2的基础,主要特性都在HTTP/2之中得到继承。

2015年,HTTP/2发布。它不叫HTTP2.0,是因为标准为原味不打算再发布子版本了,下一个新版本将是HTTP/3。

二进制协议

HTTP1.1版的头信息肯定是文本(ASCII编码),数据体可以是文本,也可以是二进制。HTTP/2则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为“帧”(frame):头信息帧和数据帧。

二进制协议的一个好处是,可以定义额外的帧。HTTP/2定义了近十种帧,为将来的高级应用打好了基础。如果使用文本实现这种功能,解析数据将会变得非常麻烦,二进制解析则方便得多。

多工

HTTP2复用TCP连接,在一个连接i,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序—对应,这样就避免了“队头堵塞”。

举例来说,在一个TCP连接里面,服务器同时收到了A请求和B请求,于是先回应A请求,结果发现处理过程非常耗时,于是就发送A请求已经处理好的部分,接着回应B请求,完成后,再发送A请求剩下的部分。

这样双向的、实时的通信,就要做多工(Multiplexing)。

数据流

因为HTTP/2的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的回应。因此,必须要对数据包做标记,指出它属于哪个回应。

HTTP/2将每个请求或回应的所有数据包,称为一个数据流(stream)。每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流ID,用来区分它属于哪个数据流。另外还规定,客户端发出的数据流,ID一律为奇数,服务端发出的ID为偶数。

数据流发送到一半的时候,客户端和服务器都可以发送信号(RST_STREAM帧),取消这个数据流。1.1版取消数据流的唯一方法,就是关闭TCP连接。这就是说,HTTP/2可以取消某一次请求,同时保证TCP连接还打开着,可以被其他请求使用。

客户端还可以指定数据流的优先级。优先级越高,服务器就会越早回应。

头信息压缩

HTTP协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如Cookie和User Agent,一模一样的内容,每次请求都必须附带,这会浪费很多宽带,也影响速度。

HTTP/2对这一点做了优化,引入了肉信息压缩机制(header compression)。一方面,头信息使用gzip或compress压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段,只发送索引号,这样就提高速度了。

服务器推送

HTTP/2允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送(server push)。

常见场景是客户端请求一个网页,这个网页里面包含很多静态资源。正常情况下,客户端必须收到网页后,解析HTML源码,发现有静态资源,再发出静态资源请求。其实,服务器可以预期到客户端请求网页后,很可能会再请求静态资源,所以就主动把这些静态资源随着网页一起发送给客户端了。

HTTP 缓存

在任何一个前端项目中,访问服务器获取数据都是很常见的事情,但是如果想吐的数据被重复请求了不止一次,那么多余的请求次数必然会浪费网络带宽,以及延迟浏览器渲染所要处理的内容,从而影响用户的使用体验。如果用户使用的是按量计费的方式访问网络,那么多余的请求还会隐性的增加用户的网络流量资费。因此考虑使用缓存技术对已获取的资源进行重用,是一种提升网站性能与用户体验的有效策略。

缓存的原理是在首次请求后保存一份请求资源的响应副本,当用户在此发起相同请求后,如果判断缓存命中则拦截请求,将之前存储的响应副本返回给用户,从而避免重新香服务器发起资源请求。

缓存的技术种类有很多,比如代理缓存、浏览器缓存、网管缓存、负载均衡器及内容分发网络等,它们大致可以分为两类:共享缓存和私有缓存。共享缓存指的是缓存内容可以被多个用户使用,如公司内部架设的Web代理;私有缓存指的是只能单独被用户使用的缓存,如浏览器缓存。

HTTP缓存应该算是前端开发中最常接触的缓存机制之一,它又可细分为强制缓存与协商缓存,二者最大的区别在于判断缓存命中时,浏览器是否需要服务器进行询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求。

强制缓存

对于强制缓存而言,如果浏览器判断所请求的目标资源有效命中,则可直接从强制缓存中返回请求响应,无须与服务器进行任何通信。

如果缓存时间有效,直接就地复用。设置强制缓存有两种方式

  1. expires: 2021-11-08 21:39 //utc的时间戳
  2. cache-control:max-age=5 单位是秒

expires有缺陷,是用服务器的时间和客户端的时间做比对,可能时间会出现不同步。

cache-control:max-age,是滑动时间,用时间戳来表示缓存时间。

协商缓存

协商缓存就是在使用本地缓存之前,需要想服务器端发起一次GET请求,与之协商当前浏览器保存的本地缓存是否已经过期。

缓存是服务端设置的,除非用node中转,做中间层,可以去设置。静态资源可以用nginx处理,动态资源一般由服务端处理。

协商缓存过程如下:

  1. 告诉客户端该资源要使用协商缓存 Cache-control: no-cache
  2. 服务器要下发一个字段告诉客户端这个资源的更新时间    last-modified: xxxx-xx UCT时间戳
  3. 客户端使用缓存数据之前问一下服务器缓存有效吗
  4. 服务端:(有效)返回304,客户端使用本地缓存资源。(无效):直接返回新的资源数据,同时设置新的缓存时间
Cache-control: no-cache //告诉客户端要进行协商缓存
last-modified: xxxx-xx //告诉客户端资源的更新时间
//再次进入时,服务端根据if-Modified-Since这个字段传递的数据与更新时间做比对,确认数据资源无误,使用缓存,文件内容没变,而文件名更新也会造成去服务器请求数据

//优化:
Cache-control: no-cache //告诉客户端要进行协商缓存
etag: xxxxx //把该资源的内容密码戳发给客户端

//再次进入时,服务端根据if-none-macth这个字段传递的数据与etag做比对,确认数据资源无误,使用缓存