由前文内容浏览器工作原理与事件循环引出的问题:当我们的页面足够复杂,足够大时,如何使页面更快展示内容呢?现在在本文来做一次抛砖引玉。若有其他加载优化,希望评论区不吝赐教。
首先,我们需要先了解的是浏览器网络进程和渲染进程。(具体内容参照文章:浏览器是如何渲染页面的?)接下来,我会根据每一个渲染阶段提出可以优化的内容。
一.解析HTML阶段
解析HTML阶段,浏览器可能会遇到需要加载外部资源的 script
标签与 link
标签。浏览器会通过网络进程去加载 JS
,CSS
等外部资源,若JS资源较大,加载时间过长可能会导致因等待资源而无法继续解析HTML。(解析CSS生成CSSOM树不会改变DOM树,所以HTML解析不会阻塞,解析JS也不会改变DOM树,但是执行JS可能会改DOM树和CSSOM树,所以CSSOM树解析好后才会执行JS。因此JS会阻塞HTML解析,CSS不会阻塞但可能因JS而暂停HTML解析。)
解析顺序图如下:
因此,为了尽可能防止HTML解析被阻塞,我们需要人为干涉浏览器的默认操作。我暂时能想到的就是两个方面:
- 异步加载(
defer
、async
、module
)和预加载(preload
、prefetch
、dns-prefetch
、preconnect
、prerender
)。 - HTTP2里请求的多路复用。
1.异步加载
使用异步加载是在 script
标签上增加对应字段。如下图所示:
根据上图,我们可以知道以下信息:
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
访问到它的值。
上面是使用异步加载的字段效果,那么,我们怎么判断何时使用 defer
或 async
字段呢?这里我们需要引入一对新的概念,DOMContentLoaded
和 load
。这两个概念是什么?可以查看MDN官方给出的解释:Document:DOMContentLoaded 事件 和 Window:load 事件。
由以上解释可知,DOMContentLoaded
是在 DOM
加载完成时触发,而 load
是在整个页面及所有依赖资源如样式表和图片都已完成加载时触发。因此,load
事件是在DOMContentLoaded
事件之后触发的。结合 defer
和 async
字段可得下图事件时间轴:
由上面的事件时间轴可知:
- async字段的JS会在加载完JS后立即执行,最迟也会在load事件前执行完。
- 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;
对于每一类资源浏览器都有一个默认的加载优先级规则:
html
、css
、font
这三种类型的资源优先级最高;- 然后是
preload
资源(通过标签预加载)、
script
、xhr
请求; - 接着是图片、语音、视频;
- 最低的是
prefetch
预读取的资源。
下图总结了资源优先级计算后各类资源的优先级情况,其中特别将上面讲的三种常见资源的情况框了出来。红框框中的为脚本类型、紫框的为图片类型、蓝框为XHR请求。图片来源点此。
由上我们引出了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 。这个属性在