JavaScript阻塞

一、阻塞特性

《高性能JavaScript》一书中,关于第一章“Loading and Execution”,提到了无阻塞加载JavaScript技术,目的是为了提高页面呈现速度。

说到无阻塞加载JavaScript要点,我们就有必要知道,为什么在html中不管是内联JavaScript还是外联,会影响到页面的性能?

原因是:JavaScript是单线程,在JavaScript运行时其他的事情不能被浏览器处理。事实上,大多数浏览器使用单线程处理UI更新和JavaScript运行等多个任务,而同一时间只能有一个任务被执行。所以在执行JavaScript时,会妨碍其他页面动作。这是JavaScript的特性,我们没法改变。

并且,html解析过程是至上而下的,当html解析器遇到诸如<script>、<link>等标签时,解析器就会停止下来,去下载相应的内容。需要注意的是,在加载<script>、<link>标签时都会阻止解析器往下执行。

并且,html解析过程是至上而下的,当html解析器遇到诸如<script>、<link>等标签时,就会去下载相应内容。且加载、解析、执行JavaScript会阻止解析器往下执行。

那什么时候,html解析器才能往下继续解析html文档呢?

就JavaScript而言,当html解析器遇到<script>标签,无论它是内联还是外联,页面中的下载和解析过程都必须停止,直到<script>从外部加载进来的JavaScript或内联的JavaScript运行完毕,方可继续解析。在高版本的浏览器当中,允许并行下载JavaScript文件,当一个<script>标签正在下载外部资源时,不必阻塞其他<script>标签,但是不幸地是,JavaScript的下载仍然会阻塞其他资源的下载,例如图片。这里还需要值得注意的是,对于样式和脚本的先后顺序同样会影响到浏览器的解析过程,比如将<link>标签放在<script>标签前面,如果样式下载受阻,那么将阻塞<link>后面的<script>加载和执行,究其原因主要在于:script脚本在执行过程中可能会引用到相关样式。

了解了JavaScript在html中的阻塞特性,我们再来看看如何改善其阻塞特性。

二、改善方法

--最简单做法--:

为了让html文档在解析时,尽量地快,常规的做法是将<script>标签放到</body>标签的前面,这样就不会阻塞html中其他资源的下载了。

如下:

尽管脚本下载之间互相阻塞,但页面已经下载完成并且显示在用户面前了,进入页面的速度不会显得太慢。且,为了让脚本之间的互相阻塞最小化,通常将多个相关的JavaScript文件合并为一个JavaScript文件,另外这样做带来的好处不仅让脚本之间阻塞变小,还减少了http请求的数量。

但,这样做JavaScript文件下载之间还是会阻塞,特别是当JavaScript文件逐渐变多时。

故而,引入无阻塞脚本技术。

无阻塞脚本技术主要分为两大类:

  1、  HTML5中的defer和async;

  2  动态创建script为dom元素。

下面将分别介绍。

--HTML5中的defer和async--:

HTML5中提供了两个属性供<script>标签使用,目的就是为了无阻塞加载JavaScript。

用法如下:

<script src="file1.js" defer></script>
<script src="file2.js" async></script>

需要注意的是,这两个属性对内联JavaScript是无效的,只针对外联JavaScript,如上所示。

加载流程:

当解析器遇到设置defer或者async属性的<script>元素时,它开始下载脚本,并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器没有停下来等待他下载。

defer和async区别:

就defer和async的区别而言,使用defer的<script>标签是按照他们排列的顺序执行的,而使用async的<script>标签是不按他们在HTML中的排列顺序执行的;

就执行时间而言,defer是在DOMContentloaded事件之前执行,而async是在window.onload事件之前执行的,且只支持IE10+。当defer和async同时存在时,会忽略defer而遵循async。且使用defer和async的脚本禁止使用document.write方法哦。

--动态脚本元素--:

因为script标签是在html中的,是属于dom元素,所以我们完全可以利用dom方法创建一个动态的script元素。

如下:

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'file1.js';
document.getElementsByTagName('head')[0].appendChild(script); 

“当创建的script元素添加到页面后立刻开始下载。此技术的重点在于:无论在何处启动下载,文件的下载和运行都不会阻塞其他页面的处理过程。你甚至可以将这些代码放在<head>部分而不会对其余部分的页面代码造成影响(除了用于下载文件的HTTP连接)”

上面加粗部分引至《高性能JavaScript》,当时在我读到这句话时,不是很理解,在前面“阻塞特性”一小节中,我们提到JavaScript是单线程且与UI线程互排,那么JavaScript在运行时,怎么不会阻塞其他页面的处理过程呢?

为此,带着这一困惑在博客园问答中心提出了自己的观点并与道友讨论(‘博问点击此’)

通过与道友讨论以及自己查看了相关文档后,有了自己见解:

之所以动态创建script元素去加载JavaScript文件,不会对页面其余操作影响,原因如下:

  1、html解析器将script当做了dom元素,而不是script标签,所以就不对其进行诸如加载、解析、运行时,停止页面中一切行为。打了个擦边球。

  2、JavaScript是单线程,且与UI线程共享同一个线程,但这不代表浏览器就只有一个线程。所以在执行JavaScript代码时,不影响图片之类的下载。

好了,回到刚才采用动态脚本元素的方法,我们还得完善下,原因是上述代码,在‘自运行’时还好,但是如果引用了其他js文件中的方法呢?那就得出错咯。因为我们无法保证动态脚本元素执行JavaScript代码的顺序。针对这一问题,标准浏览器我们可以利用<script>节点的load事件处理,而IE浏览器我们可以利用其特有的readystatechange事件处理。

封装好的代码如下:

复制代码
function loadScript(url, callback){
    var script = document.createElement('script');
    script.type = 'text/javascript';
    /*
        在IE中readyState值所表示的最终状态并不一致,
        有时<script>元素会得到"loaded"却不出现"complete",
        但另外一些情况下出现"complete"而用不到"loaded"。
        最安全的办法就是在readystatechange事件中检查这两种状态,
        并且当其中一种状态出现时,删除readystatechange事件句柄(保证事件不会被触发两次)
    */
    if(script.readyState){//IE
        script.onreadystatechange = function(){
            if(script.readyState == 'loaded' || script.readyState == 'complete'){
                script.onreadystatechange = null;
                callback()
            }
        }
    }else{//Other
        script.onload = function(){
            callback();    
        }
    }
    script.src = url;
    document.getElementsByTagName('head')[0].appendChild(script);
}
复制代码

所以,当页面中动态加载多个有关联的JavaScript文件时,我们可以将其串联起来,保证顺序。

如下:

复制代码
//串联起来
loadScript('file1.js',function(){
    loadScript('file2.js',function(){
        ...
    });
});
复制代码

除开这种方法,还有一种就是“XHR脚本注入”,大体内容与上面的方法差不多,都需要动态创建script元素,区别在于该方法利用XMLHttpRequest对象,请求JavaScript文件,并将请求到的responseText,插入script元素的text中。因为是借助XMLHttpRequest对象,缺点显而易见,不能跨域请求。

示例代码如下:

复制代码
var xhr = new XMLHttpRequest();
xhr.open('get', 'file1.js', true);
xhr.onreadystatechange = function(){
    if(xhr.readyState == 4){
        if(xhr.status >= 200 && xhr.status < 300 || xhr.status ==304){
            var script = document.createElement('script');
            script.type = 'text/javascript';
            script.text = xhr.responseText;
            document.body.appendChild(script);
        }
    }
};
xhr.send(null);
复制代码
三、拓展阅读

 [1] JavaScript是单线程的深入分析

 [2] HTML渲染过程详解

 [3] 浏览器加载渲染网页过程解析

 [4] defer、async属性以及JS异步加载并执行解决方案

 [5] HTML5 <script>元素async,defer异步加载

 [6] 无阻赛JavaScript脚本技术

### 浏览器在 JavaScript 阻塞情况下的 HTML 和 CSS 解析与渲染机制 #### 1. **JavaScript 阻塞 DOM 解析的过程** 当浏览器解析 HTML 文档时,一旦遇到 `<script>` 标签(未设置 `defer` 或 `async` 属性),会暂停当前的 DOM 解析操作[^2]。这是因为传统的同步脚本需要立即被执行,而其执行结果可能会影响后续 DOM 结构的生成。例如,脚本中可能存在动态修改文档的行为,如插入新元素或删除已有元素。 ```javascript // 示例:同步脚本可能导致 DOM 解析中断 document.write('<p>This is dynamically added content.</p>'); ``` 在这种情况下,浏览器必须等待该脚本加载并执行完成后,才会继续解析剩余的 HTML 内容[^3]。 --- #### 2. **CSS 加载对 JavaScript 执行的影响** 如果 `<script>` 标签前存在尚未加载完成的外部 CSS 文件,浏览器会优先等待这些 CSS 完成加载后再执行 JavaScript 脚本[^4]。这是为了避免脚本访问到不完整的样式信息,从而返回错误的结果。例如,在脚本中调用 `getComputedStyle()` 方法时,若某些样式还未应用,则可能导致逻辑异常。 ```javascript // 示例:脚本依赖于已完成加载的 CSS 样式 const style = window.getComputedStyle(document.getElementById('example')); console.log(style.color); ``` 因此,即使 JavaScript 已经准备好运行,只要前面有未加载完毕的 CSS,浏览器仍会延迟脚本的执行直到所有必要的样式资源可用为止[^1]。 --- #### 3. **渲染树生成与页面渲染的关系** 尽管 CSS 不会直接阻塞 DOM 的解析,但它确实会阻止渲染树的生成,因为渲染树需要同时基于 DOM 树和 CSSOM 树构建[^5]。这意味着即便 DOM 解析已经完成,如果没有全部所需的样式规则到位,浏览器也无法开始实际的页面渲染过程。 具体而言: - 当浏览器发现某个区域缺少必要样式时,它会选择暂时隐藏这部分内容,而不是以默认样式展示出来。 - 这种行为有助于提供更一致的用户体验,防止所谓的“无样式内容闪现”(Flash of Unstyled Content, FOUC)现象发生。 --- #### 4. **解决方法与最佳实践** 为了减轻 JavaScript 对页面加载速度带来的负面影响,开发者可以采取以下措施: - 将 `<script>` 标签放置在文档末尾接近 `</body>` 处,以便让大部分可见内容先行加载[^2]。 - 使用 `defer` 属性指定脚本应在 DOM 解析结束后再执行;或者利用 `async` 属性实现脚本的异步加载而不妨碍其他资源获取[^4]。 ```html <!-- 推荐做法 --> <script src="main.js" defer></script> <link rel="stylesheet" href="styles.css"> ``` 此外,合理安排 CSS 和 JavaScript 的引入顺序也很重要,通常建议将 `<link>` 放置在 `<head>` 中,而将非关键路径上的 `<script>` 移动到底部附近[^3]。 --- ### 总结 综上所述,浏览器在面对 JavaScript 阻塞情形时遵循严格的处理原则:暂停 DOM 解析直至脚本完成执行,并确保所有前置依赖(特别是 CSS)均已就绪后才允许进一步的操作。这种设计虽增加了初始加载时间,却保障了最终呈现质量的一致性和可靠性。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值