浏览器线程阻塞和无阻塞加载脚本的理解

一个页面,从被请求访问,到用户能够看到页面、操做页面,到最后页面彻底加载完毕,中间须要经历一个至关奇幻的过程,这个过程的速度被“web性能师”孜孜不倦、前赴后继的优化。本文讨论的是其中一个优化。node

浏览器线程和阻塞

虽然你们耳熟能详的一句话是:web

JavaScript是单线程的。跨域

可是:浏览器

浏览器固然不是单线程的。性能优化

浏览器的多线程中,有的线程负责加载资源,有的线程负责执行脚本,有的线程负责渲染界面,有的线程负责轮询、监听用户事件。网络

这些线程,根据浏览器自身特色以及web标准等等,有的会被浏览器特地的阻塞。两个很明显的阻塞就是:脚本执行时对其余线程的阻塞脚本加载时对其余线程的阻塞多线程

这两个阻塞发生在HTML页面初次解析时,它们对性能的影响较大,缘由是:并发

document对象绑定了一个事件:DOMContentLoaded。这个事件会在DOM解析完成以后触发。这个事件触发以后(而不是window.load事件1),会进入异步事件驱动阶段(另外一个线程控制)。也就是说,DOM解析工做不完成,用户与页面的不少(并非全部)事件交互就没法进行。这时候浏览器的忙指示(那个页面上方的烦人的旋转的圆圈)不会消失。app

咱们先从执行脚本时的阻塞提及。异步

<!--more-->

执行脚本带来的阻塞

众所周知,浏览器中有两个引擎——JavaScript引擎和渲染引擎,它们对应了浏览器的两个线程。这两个引擎各司其职:

  1. JavaScript引擎解析并执行JavaScript代码。

  2. 渲染引擎对界面进行绘制或者重绘(对DOM的渲染)。

在浏览器取得HTML文档并解析HTML的时候,浏览器会:

  1. 对DOM树进行解析。解析<body>中标签内容时,能造成渲染树,并渲染其中的元素。这个是在渲染引擎中作的。

  2. 遇到<script>标签时(注意解析HTML时遇到的script标签),基于JavaScript可能会修改DOM的考虑,其中的内容将会在此时被执行(若是是外部JS文件的代码,会先加载这个文件资源,这部分后面再表)。执行JavaScript代码是在JavaScript引擎中作的。

因为:

脚本执行和渲染DOM的并发可能会引起严重的冲突,

因此:

JavaScript引擎和渲染引擎所在的两个线程被设计为互斥的!

这就意味着:

在执行<script>中内容时,浏览器会切换到JavaScript引擎所在的线程,此时渲染引擎所在的线程会阻塞,故其后元素的解析和渲染会暂停。这时候若是脚本执行时间太长的话,不只后面的元素会一直看不到,对DOM的解析工做也会一直完不成。用户会陷入焦急的等待中。

解决这个问题的一个经典思路,就是:

<script>放到紧跟</body>以前的位置。这样就不会影响须要放到页面上的UI元素的解析了。这样的好处就是,用户能即便看到页面上的UI元素,而防止出现了浏览器白屏等现象。

加载资源带来的阻塞

再来讲说加载。

加载是浏览器从网络中请求资源(好比图片、样式文件、静态脚本等),将资源进行相应的处理。

与上述说的两个线程不一样,对资源进行加载的线程通常不会和上述两个线程互斥。例如图片资源就能够并行下载,下载的最大并行数量与浏览器的配置有关系。(这里还有一个知识点,下载的最大并行数指的是从一个主机上下载的最大并行数,若是从多个主机下载资源,这个数量会翻倍,可是因为对DNS的解析也是一个性能优化的点,故而通常策略是:不该设置超过4个主机,最好只设置2个主机)。

不会互斥意味着:资源的加载能够和UI渲染、重排,事件响应,或者JavaScript代码的执行的并发进行。

可是操蛋的就是,若是浏览器解析DOM时须要下载脚本资源,那么下载这个资源的线程就是阻塞其余下载线程以及渲染线程,致使渲染速度变慢。

为了解决这个问题,咱们依然是将<script>标签放到最后。

可是假设该脚本下载的速度较慢,并且多个脚本非并发下载,而且假如多个<script>内脚本执行时间较长的话,DOM解析工做仍是会一直完不成。

故而咱们须要无阻塞加载脚本的技术。

无阻塞加载脚本之一——defer

将问题暴露出来以后,咱们能够根据其阻塞的缘由反向想出解决思路。

咱们可否将脚本资源文件像图片同样并发的加载而不是让渲染线程挂起等待?道理上是能够的。之因此要让它阻塞等待,是由于担忧JavaScript脚本会修改DOM。因此若是咱们对其比较放心的话,是没必要让渲染线程等待的。

这就是原生的defer思路:为<script>标签配置一个defer属性(这是个bool属性,配置以后表示为true),这代表咱们知道脚本内没有相似document.write的方法,此时标签内的脚本就会并行的加载,而且在DOM解析完毕以后(即document.readyState变为inactive以后和上述的DOMContentLoaded事件触发以前),将脚本执行。注意:不一样脚本的执行顺序,是按照不一样脚本相应的<script>在HTML中出现的顺序决定的

<script src='..' defer></script>

可是defer在不一样浏览器中的支持程度不一样。咱们目前还不能特别依赖它。

defer的另外一个缺点是,下载的脚本,其执行顺序和<script>标签在HTML中出现的顺序是一致的(同步的),而且执行脚本时也会阻塞其余线程(这个没法优化)。

另外一个更加没有获得支持的属性是async,它跟defer相似,可是它是异步的。比同步的defer更快一步。咱们在这里不讨论async为何不能被支持的问题,可是咱们接下来的技术跟async的步骤是类似的。

无阻塞加载脚本之二——动态脚本元素

所谓的动态脚本元素,就是说<script>标签不是写死在HTML中的,而是由现有的脚本生成的。

为何能够作到这样呢?由于<script>标签也是DOM元素的一种,而JavaScript是能够经过DOM API操做DOM的。

不一样于静态脚本元素的解析,动态脚本元素在下载的时候是不会阻塞渲染线程的,也就是实现了并行下载。

<script>
    
        var node = document.createElement('script');
        
        node.src = '...';
        
        document.head.appendChild(node);
        
 </script>

这个标签能够放到</body>以前。

document.head.appendChild代码以后,因为没有触发渲染树的重绘,切换回的渲染线程会将剩下的DOM解析并渲染完毕。同时新插入的<script>中的资源也会并发的下载。

那么脚本在何时执行呢?

答案是<script>中的资源下载完以后会立刻执行。可是因为此时DOM已经解析完毕,而且进入异步事件阶段,因此即便切换到JavaScript引擎所在的线程上执行脚本,用户也不会感受明显的UI阻塞。

因为资源的大小不一样,因此这些脚本的执行将会是异步的

因为脚本的异步执行,那么如何解决脚本之间先后依赖的问题呢?咱们天然就会想到回调函数。在脚本加载执行完毕后,会触发该<script>元素的onload事件,咱们能够将回调放到这个事件中处理。

无阻塞加载脚本之三——XMLHttpRequest

这种方法的局限很明显:没法跨域

可是若是是非跨域的脚本,咱们可使用XMLHttpRequest请求,将脚本放到responseText中,而且将其放到生成的<script>中.

xhr.onreadystatechange = function(){

    if (xhr.readyState == 4){
    
        var node = document.createElement('script');
        
        node.text = xhr.responseText;
        
        document.head.appendChild(node);
    }
}

这种方法一样能够异步的加载并执行脚本。

结束

上述是我本身的微小的看法。若有错误,欢迎指正。


  1. 自信源于犀牛书:《JavaScript权威指南(中文第六版)》326页,13章。
相关文章
相关标签/搜索