提升页面的常用方法
- 资源压缩合并,减少http请求,开启gzip压缩
- 非核心代码异步加载
- 利用浏览器缓存
- 使用CDN
- 预解析DNS
- 域名收敛
- 页面首屏优化(白屏优化、FMP、TTI)
- 组件优化(懒加载、预加载、Keep-alive)
1 | <!-- off为关闭,ON为开启,页面中所有的a标签默认开启dns预解析,但是如果页面是https的,浏览器默认关闭预解析,这里最好开启一下 --> |
异步加载
方法
- 动态脚本加载(动态创建节点)
- defer
- async
defer、async区别
defer是在HTML解析完之后才会执行,如果是多个,则按照加载的顺序依次执行。
async加载完之后立即执行,如果是多个 ,执行顺序和加载顺序无关。
1 | <!-- defer1和async1文件内容很大--> |
运行结果:
- write
- 1
- 2
- 3
- async2
- defer1
- defer2
- async1
浏览器缓存
缓存是提升页面性能同时减少服务器压力的利器。
分类
- 强缓存
- 协商缓存
强缓存(直接拿缓存结果使用)
不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network选项中可以看到该请求返回200的状态码,并且size显示from disk cache或from memory cache;
强缓存相关的header
Expires: Wed, 22 Aug 2018 09:38:37 GMT
Cache-Control: Cache-Control:max-age=3600
Expires标识绝对时间,一般是服务下发的服务器绝对时间,它的值为一个绝对时间的GMT格式的时间字符串
Cache-Control是相对时间,在配置缓存的时候,以秒为单位,用数值表示。当值设为max-age=3600时,代表在在获取到资源之后,在3600s之内再次加载资源,就会命中强缓存,不会向服务器重新请求资源。
如果服务器将两个字段都下发,则以Cache-Control为准
强缓存比较
其实这两者差别不大,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容较客户端是否已经发生了更新呢?此时我们需要协商缓存策略。
协商缓存(使用缓存文件之前先请求服务器看是否可以使用)
向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;另外协商缓存需要与cache-control共同使用。
协商缓存相关的header
第一组:
Last-Modified 上次修改的时间。当强缓存失效,客户端开始请求,携带Last-Modified-Since(值为服务器下发的
Last-Modified的值)
(问题是时间可能变了 但是内容可能没有变化)
Last-Modified-Since
Last-Modified和If-Modified-Since:当第一次请求资源时,服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端。Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT 客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查,若传递的时间值与服务器上该资源最终修改时间是一致的,则说明该资源没有被修改过,直接返回304状态码,内容为空,这样就节省了传输数据量 。如果两个时间不一致,则服务器会发回该资源并返回200状态码,和第一次请求时类似。这样保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。一个304响应比一个静态资源通常小得多,这样就节省了网络带宽。

【Last-Modified,If-Modified-Since】都是根据服务器时间返回的header,一般来说,在没有调整服务器时间和篡改客户端缓存的情况下,这两个header配合起来管理协商缓存是非常可靠的,但是有时候也会服务器上资源其实有变化,但是最后修改时间却没有变化的情况,而这种问题又很不容易被定位出来,而当这种情况出现的时候,就会影响协商缓存的可靠性。总接起来是以下两点:
Ⅰ.某些服务端不能获取精确的修改时间
Ⅱ.文件修改时间改了,但文件内容却没有变
所以就有了另外一对header来管理协商缓存,这对header就是【ETag、If-None-Match】。
第二组:
Etag 服务器下发 ,当强缓存失效,客户端开始请求,携带If-None-Match(值为服务器下发的Etag的值)
If-None-Match
Etag是上一次加载资源时,服务器返回的response header,是对该资源的一种唯一标识,只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。

两组之间对比:
首先在精确度上,Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
第三在优先级上,服务器校验优先考虑Etag
1 | Cache-Control > Expires > Etag(If-None-Match) > Last-Modified(Last-Modified-Since) |
浏览器缓存机制
强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存。主要过程如下:

域名收敛
如果你页面中引入的各种资源来自不同的域名,注意每增加一个域名,都会增加一次域名解析开销。 在复杂的国内移动互联网网络环境下,不同域名的解析速度可能会相差数十倍。 所以你需要有意识的收敛页面资源所需解析的域名数, 特别是会阻塞页面渲染的CSS,JS,Font资源。 很多性能糟糕页面究其原因或许会是引入的资源域名解析速度很慢或完全不能正确解析。在我们的实践中, 一个页面所产生的域名解析数不能超过5个。
页面首屏优化(白屏优化、FMP、TTI)
前端应用往往是实时加载执行的,并不需要预先下载,这就造成了一个问题,前端开发中往往最影响性能的不是什么计算或者渲染,而是加载速度,加载速度会直接影响用户体验和网站留存。
白屏定义:从首次绘制(First Paint,FP)算起到首次内容绘制(First Contentful Paint,FCP)这段时间算白屏,也有不少前端jser认为从路由改变起(即用户再按下回车的瞬间)到首次内容绘制(即能看到第一个内容)为止算白屏时间
1 | /* |
1 | 白屏时间 = firstPaint(performance.timing.responseStart) - performance.timing.navigationStart |
白屏加载问题的归根原因:网络、带宽、浏览器最多并发6个请求、资源过大、SPA
白屏优化手段
- loading 提示
- 骨架屏
- ssr 同构
- 开启浏览器缓存(这块儿结合打包以及PWA Service Worker缓存性能会有大幅度提升)
- 开启 HTTP2
这里说一下HTTP2
1) http2采用二进制分帧的方式进行通信,而 http1.x 是用文本,http2 的效率更高
2) http2 可以进行多路复用,即跟同一个域名通信,仅需要一个 TCP 建立请求通道,请求与响应可以同时基于此通道进行双向通信,而 http1.x 每次请求需要建立TCP,多次请求需要多次连接,还有并发限制,十分耗时
3) http2 可以头部压缩,能够节省消息头占用的网络的流量,而HTTP/1.x每次请求,都会携带大量冗余头信息,浪费了很多带宽资源
4) http2可以进行服务端推送,我们平时解析 HTML 后碰到相关标签才会进而请求 css 和 js 资源,而 http2 可以直接将相关资源直接推送,无需请求,这大大减少了多次请求的耗时
FMP(首次有意义绘制)
在白屏结束之后,页面开始渲染,但是此时的页面还只是出现个别无意义的元素,在FCP 和 FMP 之间虽然开始绘制页面,但是整个页面是没有意义的,用户依然在焦虑等待,而且这个时候可能出现乱序的元素或者闪烁的元素,很影响体验。Skeleton是一个好方法,Skeleton现在已经很开始被广泛应用了,它的意义在于事先撑开即将渲染的元素,避免闪屏,同时提示用户这要渲染东西了,较少用户焦虑。(vue-skeleton-webpack-plugin)
TTI(可交互时间)
当有意义的内容渲染出来之后,用户会尝试与页面交互,这个时候页面并不是加载完毕了,而是看起来页面加载完毕了,事实上这个时候 JavaScript 脚本依然在密集得执行
TTI 到来之后用户就可以跟页面进行正常交互的,TTI 一般没有特别精确的测量方法,普遍认为满足FMP && DOMContentLoader事件触发 && 页面视觉加载85%这几个条件后,TTI 就算是到来了
在页面基本呈现到可以交互这段时间,绝大部分的性能消耗都在 JavaScript 的解释和执行上,这个时候决定 JavaScript 解析速度的无非一下两点
- JavaScript 脚本体积
- JavaScript 本身执行速度
这对这两个问题优化手段其实有很多,结合webpack有一些现成的优化方案 https://lozoe.github.io/2020/03/31/webpack%E4%BC%98%E5%8C%96/
组件优化(懒加载、预加载、Keep-alive)
- 组件懒加载
lazy + Suspense 懒加载 + 动态加载
1 | // 动态加载图表组件 |
2.组件预加载
通过组件懒加载将页面的初始渲染的资源体积降低了下来,提高了加载性能,但是组件的性能又出现了问题
在用户的鼠标还处于 hover 状态的时候就开始触发图表资源的加载,通常情况下当用户点击结束之后,加载也基本完成,这个时候图表会很顺利地渲染出来,不会出现延迟.
1 | /** |
3.keep-alive
在页面已经跳转后依然不销毁组件,保存组件对应的实例在内存中,当此页面再次需要渲染的时候就可以利用已经缓存的组件实例。
参考:
https://juejin.im/entry/58db1a20b123db199f53005e
https://juejin.im/post/5d00820b5188255ee806a1c7#heading-5
https://zhuanlan.zhihu.com/p/82981365