一个页面,从被请求访问,到用户可以看到页面、操作页面,到最后页面完全加载完毕,中间需要经历一个相当奇幻的过程,这个过程的速度被 "web 性能师" 孜孜不倦、前赴后继的优化。本文讨论的是其中一个优化。
虽然大家耳熟能详的一句话是:
但是:
浏览器的多线程中,有的线程负责加载资源,有的线程负责执行脚本,有的线程负责渲染界面,有的线程负责轮询、监听用户事件。
这些线程,根据浏览器自身特点以及 web 标准等等,有的会被浏览器特意的阻塞。两个很明显的阻塞就是:脚本执行时对其他线程的阻塞和脚本加载时对其他线程的阻塞。
这两个阻塞发生在 html 页面初次解析时,它们对性能的影响较大,原因是:
对象绑定了一个事件:
- document
。这个事件会在 DOM 解析完成之后触发。这个事件触发之后(而不是
- DOMContentLoaded
事件),会进入异步事件驱动阶段(另一个线程控制)。也就是说,DOM 解析工作不完成,用户与页面的很多(并不是所有)事件交互就无法进行。这时候浏览器的忙指示(那个页面上方的烦人的旋转的圆圈)不会消失。
- window.load
我们先从执行脚本时的阻塞说起。
<!--more-->
众所周知,浏览器中有两个引擎——JavaScript 引擎和渲染引擎,它们对应了浏览器的两个线程。这两个引擎各司其职:
在浏览器取得 HTML 文档并解析 HTML 的时候,浏览器会:
由于:
所以:
这就意味着:
在执行
中内容时,浏览器会切换到 JavaScript 引擎所在的线程,此时渲染引擎所在的线程会阻塞,故其后元素的解析和渲染会暂停。这时候如果脚本执行时间太长的话,不仅后面的元素会一直看不到,对 DOM 的解析工作也会一直完不成。用户会陷入焦急的等待中。
- <script>
解决这个问题的一个经典思路,就是:
把
放到紧跟
- <script>
之前的位置。这样就不会影响需要放到页面上的 UI 元素的解析了。这样的好处就是,用户能即使看到页面上的 UI 元素,而防止出现了浏览器白屏等现象。
- </body>
再来说说加载。
加载是浏览器从网络中请求资源(比如图片、样式文件、静态脚本等),将资源进行相应的处理。
与上述说的两个线程不同,对资源进行加载的线程一般不会和上述两个线程互斥。例如图片资源就可以并行下载,下载的最大并行数量与浏览器的配置有关系。(这里还有一个知识点,下载的最大并行数指的是从一个主机上下载的最大并行数,如果从多个主机下载资源,这个数量会翻倍,但是由于对 DNS 的解析也是一个性能优化的点,故而一般策略是:不应设置超过 4 个主机,最好只设置 2 个主机)。
不会互斥意味着:资源的加载可以和 UI 渲染、重排,事件响应,或者 JavaScript 代码的执行的并发进行。
但是操蛋的就是,如果浏览器解析 DOM 时需要下载脚本资源,那么下载这个资源的线程就是阻塞其他下载线程以及渲染线程,导致渲染速度变慢。
但是假设该脚本下载的速度较慢,而且多个脚本非并发下载,并且假如多个
内脚本执行时间较长的话,DOM 解析工作还是会一直完不成。
- <script>
故而我们需要无阻塞加载脚本的技术。
将问题暴露出来之后,我们可以根据其阻塞的原因反向想出解决思路。
我们能否将脚本资源文件像图片一样并发的加载而不是让渲染线程挂起等待?道理上是可以的。之所以要让它阻塞等待,是因为担心 JavaScript 脚本会修改 DOM。所以如果我们对其比较放心的话,是不必让渲染线程等待的。
- <script src='..' defer></script>
但是
在不同浏览器中的支持程度不同。我们目前还不能特别依赖它。
- defer
另一个更加没有得到支持的属性是
,它跟
- async
类似,但是它是异步的。比同步的
- defer
更快一步。我们在这里不讨论
- defer
为什么不能被支持的问题,但是我们接下来的技术跟
- async
的步骤是相似的。
- async
不同于静态脚本元素的解析,动态脚本元素在下载的时候是不会阻塞渲染线程的,也就是实现了并行下载。
- var node = document.createElement('script');
- node.src = '...';
- script.onload=function(){
- ...
- };
- document.getElementsByTagName("head")[0].appendChild(node);
代码之后,由于没有触发渲染树的重绘,切换回的渲染线程会将剩下的 DOM 解析并渲染完毕。同时新插入的
- document.head.appendChild
中的资源也会并发的下载。
- <script>
那么脚本在什么时候执行呢?
答案是
中的资源下载完之后会马上执行。但是由于此时 DOM 已经解析完毕,并且进入异步事件阶段,所以即使切换到 JavaScript 引擎所在的线程上执行脚本,用户也不会感觉明显的 UI 阻塞。
- <script>
由于资源的大小不同,所以这些脚本的执行将会是异步的。
由于脚本的异步执行,那么如何解决脚本之间前后依赖的问题呢?我们自然就会想到回调函数(在下载的代码包含页面其它脚本调用的接的情况下)。在脚本加载执行完毕后,会触发该
元素的
- <script>
事件,我们可以将回调放到这个事件中处理。
- onload
这种方法的局限很明显:无法跨域。
但是如果是非跨域的脚本,我们可以使用
请求,将脚本放到
- XMLHttpRequest
中,并且将其放到生成的
- responseText
中.
- <script>
- var xhr=new XMLHttpRequest();
- xhr.open("get","file.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.text = xhr.responseText;
- script.type="text/javascript";
- document.head.appendChild(script);
- }
- }
- }
这种方法同样可以异步的加载并执行脚本。
推荐的无阻塞加载脚本的做法:先加载页面初始化所需的代码;然后动态加载页面其它的功能所需的脚本。第一步尽量把代码压缩到最小尺寸
- loadScript("the-rest.js",function(){
- ...
- });
另一种方法是直接把 loadscript()函数直接嵌入页面,避免多产生一次 HTTP 请求。
来源: http://www.bubuko.com/infodetail-1985031.html