浏览器渲染页面的性能优化

  由前文内容浏览器工作原理与事件循环引出的问题:当我们的页面足够复杂,足够大时,如何使页面更快展示内容呢?现在在本文来做一次抛砖引玉。若有其他加载优化,希望评论区不吝赐教。
  首先,我们需要先了解的是浏览器网络进程和渲染进程。(具体内容参照文章:浏览器是如何渲染页面的?)接下来,我会根据每一个渲染阶段提出可以优化的内容。

一.解析HTML阶段

  解析HTML阶段,浏览器可能会遇到需要加载外部资源的 script 标签与 link 标签。浏览器会通过网络进程去加载 JSCSS 等外部资源,若JS资源较大,加载时间过长可能会导致因等待资源而无法继续解析HTML。(解析CSS生成CSSOM树不会改变DOM树,所以HTML解析不会阻塞,解析JS也不会改变DOM树,但是执行JS可能会改DOM树和CSSOM树,所以CSSOM树解析好后才会执行JS。因此JS会阻塞HTML解析,CSS不会阻塞但可能因JS而暂停HTML解析。
  解析顺序图如下:
image.png

因此,为了尽可能防止HTML解析被阻塞,我们需要人为干涉浏览器的默认操作。我暂时能想到的就是两个方面:

  1. 异步加载(deferasyncmodule)和预加载(preloadprefetchdns-prefetchpreconnectprerender)。
  2. HTTP2里请求的多路复用。

1.异步加载

  使用异步加载是在 script 标签上增加对应字段。如下图所示:
image.png

  根据上图,我们可以知道以下信息:

1.默认情况下,HTML解析的过程中,如果遇到 script 脚本,会停止解析HTML,当脚本下载解析并执行完毕后,才会继续解析HTML。
2.当使用 defer 字段时,如果遇到 script 脚本,解析 JS 会与解析HTML同时进行,JS 的执行会在解析HTML完成之后。
3.当使用 async 字段时,如果遇到 script 脚本,解析 JS 会与解析HTML同时进行,但当 JS 解析完成,会立刻停止解析HTML,转去执行 JS,当执行 JS 完毕后才重新解析HTML。
4.当使用 type="module" 字段时,相当于使用 module 模式,效果默认与 defer 字段一致,但该字段会继续解析引入模块的内容。若再加入 async 字段,效果则与单独的 async 字段一致。

  • module 默认使用了use strict 模式,这也意味着不能使用诸如 arguments.callee 这一类的语法。
  • 模块只会加载一次,无论前后你写了多少次。
  • 不支持 注释。
  • module 有自己的词法作用域,比如定义一个var a = 1,并不会创建一个全局变量,因此你并不能通过 window.a 访问到它的值。

  上面是使用异步加载的字段效果,那么,我们怎么判断何时使用 deferasync 字段呢?这里我们需要引入一对新的概念,DOMContentLoadedload 。这两个概念是什么?可以查看MDN官方给出的解释:Document:DOMContentLoaded 事件Window:load 事件
  由以上解释可知,DOMContentLoaded 是在 DOM 加载完成时触发,而 load 是在整个页面及所有依赖资源如样式表和图片都已完成加载时触发。因此,load 事件是在DOMContentLoaded 事件之后触发的。结合 deferasync 字段可得下图事件时间轴:

浏览器渲染页面的性能优化_第1张图片

  由上面的事件时间轴可知:

  1. async字段的JS会在加载完JS后立即执行,最迟也会在load事件前执行完。
  2. defer字段的JS会在HTML解析完成后执行,最迟也会在DOMContentLoaded事件前执行完,也就是DOM此时已加载完成。

  推导可知,async字段可能会导致JS执行的乱序,如果JS的执行依赖前后顺序,则不能使用async;若JS的执行不依赖于DOM的完成,可以使用async字段。若JS的执行依赖于DOM的完成,则需要使用defer字段以确保正确执行。

2.预加载

  在我们的浏览器加载资源的时候,对于每一个资源都有其自身的默认优先级,倘若我们能修改每一个资源的默认优先级,那我们几乎可以按照我们的预期加载想要加载的资源。
  资源的优先级被分为5级。不同资料上,对这5级的命名描述上可能有所不同。主要是因为资料本身可能是从网络层面,浏览器内核或者用户端控制台显示这三个方向中的某一个来说的。这三个方向虽然对这5级的命名不同,但都是一一对应的。
网络层面,5级分别为:Highest、Medium、Low、Lowest、Idle;
浏览器内核,5级分别为:VeryHigh、High、Medium、Low、VeryLow;
用户端控制台显示,5级分别为:Highest、High、Medium、Low、Lowest;

对于每一类资源浏览器都有一个默认的加载优先级规则:

  1. htmlcssfont 这三种类型的资源优先级最高;
  2. 然后是 preload 资源(通过标签预加载)、scriptxhr 请求;
  3. 接着是图片、语音、视频;
  4. 最低的是 prefetch 预读取的资源。

  下图总结了资源优先级计算后各类资源的优先级情况,其中特别将上面讲的三种常见资源的情况框了出来。红框框中的为脚本类型、紫框的为图片类型、蓝框为XHR请求。图片来源点此
浏览器渲染页面的性能优化_第2张图片

由上我们引出了preload和prefetch的概念。

preload(资源预加载):是一种浏览器机制,它通过声明向浏览器预先请求当前页后续可能需要的资源,提高这些资源的请求优先级。

    

prefetch(资源预提取):是一种浏览器机制,其利用浏览器空闲时间来下载资源或预取用户在不久的将来可能访问的文档/内容。(网页向浏览器提供一组预取提示,并在浏览器完成当前页面的加载后开始静默地拉取指定的文档并将其存储在缓存中。当用户访问其中一个预取文档时,便可以快速的从浏览器缓存中得到。--MDN)
prefetch包括资源预提取、DNS预解析、http预连接和页面预渲染。

    资源预加载:
    DNS预解析:
    http预连接: 将建立对该域名的TCP链接
    页面预渲染: 将会预先加载链接文档的所有资源

  preload与prefetch同属于浏览器的Resource-Hints(资源提示),用于辅助浏览器进行资源优化。
  preload会强制提升原本请求的优先级,使其资源更快被加载。这样做的好处是:当你使用了图片或者字体等请求优先级较低的资源时可以提升其优先级,防止页面图片抖动或者字体抖动。而prefetch会在浏览器空闲时加载其他页面的资源进入缓存,其他页面被打开时可以通过缓存快速获取资源渲染页面。
dns-prefetch(DNS预解析):尝试在请求资源之前解析域名。这可能是后面要加载的文件,也可能是用户尝试打开的链接目标。
  当浏览器从(第三方)服务器请求资源时,必须先将该跨源域名解析为 IP 地址,然后浏览器才能发出请求。此过程称为 DNS 解析。DNS 缓存可以帮助减少此延迟,而 DNS 解析可以导致请求增加明显的延迟。对于打开了与许多第三方的连接的网站,此延迟可能会大大降低加载性能。而 dns-prefetch 可帮助掩盖 DNS 解析延迟。

注: dns-prefetch 仅对跨源域上的 DNS 查找有效,因此请避免使用它来指向你的站点或域。这是因为,到浏览器看到提示时,你的站点背后的 IP 已经被解析了。

示例:

    

preconnect(http预连接):dns-prefetch 只执行 DNS 查询,而 preconnect 则是建立与服务器的连接。这个过程包括 DNS 解析,以及建立 TCP 连接,如果是 HTTPS 网站,就进一步执行 TLS 握手。
注: 如果页面需要建立与许多第三方域的连接,则将它们预先连接会适得其反。preconnect 提示最好仅用于最关键的连接。对于其他的连接,只需使用 即可节省第一步——DNS 查询——的时间。
示例:

    
    

prerender(页面预渲染):是指预取和渲染后续可能会导航到的页面。(这个渲染是浏览器在后台进行渲染,相对于一个不可见的单独选项卡。)
示例:

    
注: 预渲染是以牺牲更多浏览器资源来提高用户体验的做法,如果不能确保用户大概率甚至绝对会导航到该页面,就不能设置该字段,以免不必要的性能消耗。

  以上的优化方式,在浏览器支持的性较差,所以我们可以通过利用LocalStorage来对部分请求的数据和结果进行缓存,省去发送http请求所消耗的时间,从而提高网页的响应速度。 这类做法在移动端应用已经十分广泛。

3.对跨域资源的处理

  当我们的资源是跨域资源时,可以使用HTML属性:crossorigin 。这个属性在

注:preload的字体资源必须设置crossorigin属性,否则会导致重复加载。原因是如果不指定crossorigin属性(即使同源),浏览器会采用匿名模式的CORS去preload,导致两次请求无法共用缓存。

4.具体实践

  前文中举的例子,都是在入口html手动添加相关代码,这显然不够方便,而且将资源路径硬编码在了页面中,硬编码的方式很多场景下本身就行不通。
  ⑴ webpack插件preload-webpack-plugin可以帮助我们将该过程自动化,结合htmlWebpackPlugin在构建过程中插入link标签。

const PreloadWebpackPlugin = require('preload-webpack-plugin');
...
plugins: [
  new PreloadWebpackPlugin({
    rel: 'preload',
    as(entry) {  //资源类型
      if (/\.css$/.test(entry)) return 'style';
      if (/\.woff$/.test(entry)) return 'font';
      if (/\.png$/.test(entry)) return 'image';
      return 'script';
    },
    include: 'asyncChunks', // preload模块范围,还可取值'initial'|'allChunks'|'allAssets',
    fileBlacklist: [/\.svg/] // 资源黑名单
    fileWhitelist: [/\.script/] // 资源白名单
  })
]

  PreloadWebpackPlugin配置总体上比较简单,需要注意的是include属性。该属性默认取值'asyncChunks',表示仅预加载异步js模块;如果需要预加载图片、字体等资源,则需要将其设置为'allAssets',表示处理所有类型的资源。
但一般情况下我们不希望把预加载范围扩得太大,所以需要通过fileBlacklist或fileWhitelist进行控制。
对于异步加载的模块,还可以通过webpack内置的/_ webpackPreload: true _/标记进行更细粒度的控制。
以下面的代码为例,webpack会生成标签添加到html页面头部。

import(/* webpackPreload: true */ 'AsyncModule');
注:prefetch的配置与preload类似,但无需对as属性进行设置。

⑵vue-cli3的默认配置

  • preload

  默认情况下,一个Vue CLI应用会为所有初始化渲染需要的文件自动生成preload提示。这些提示会被@vue/preload-webpack-plugin注入,并且可以通过chainWebpack的config.plugin('preload') 进行修改和删除。

  • prefetch

  默认情况下,一个Vue CLI应用会为所有作为async chunk生成的JavaScript文件(通过动态import()按需code splitting的产物)自动生成prefetch提示。这些提示会被@vue/preload-webpack-plugin注入,并且可以通过chainWebpack的config.plugin('prefetch')进行修改和删除。

5.最佳实践:

基于上面对使用场景的分享,我们可以总结出一个比较通用的最佳实践:

  • 大部分场景下无需特意使用preload
  • 类似字体文件这种隐藏在脚本、样式中的首屏关键资源,建议使用preload
  • 异步加载的模块(典型的如单页系统中的非首页)建议使用prefetch
  • 大概率即将被访问到的资源可以使用prefetch提升性能和体验

参考资料

1.浏览器页面资源加载过程与优化-考拉海购前端团队
2.使用 Preload&Prefetch 优化前端页面的资源加载-vivo互联网技术
3.深度解析之异步加载(defer、async、module)和预加载(preload、prefetch、dns-prefetch、preconnect 、prerender)-落叶卢生
4.DOMContentLoaded-安歌的博客

你可能感兴趣的:(前端性能优化浏览器)