高性能JavaScript阅读笔记之加载和执行

207 阅读7分钟

仿佛是与生俱来的 - JavaScript 一诞生就会出现很多的性能问题。语言本身的阻塞特性再加上浏览器使用了单一的进程处理用户界面(UI)刷新和 JavaScript 脚本执行,这导致同一时刻浏览器只会做一件事情,JavaScript 脚本执行过程耗时越长,浏览器等待响应的时间就越长。

众所周知,在 HTML 页面结构代码中我们是通过 <script> 标签引入 JavaScript 脚本的。

有形如

    <script type="text/javascript" src="[url]"></script>

直接引入 JavaScript 脚本源文件地址

还有形如

    <script type="text/javascript">
        document.write("The date is " + new Date().toDateString());
    </script>

直接内嵌 JavaScript 脚本

浏览器为了保证页面处理正常,当页面解析到 <script> 标签时,就会停止对页面的处理,先执行 JavaScript 脚本,等脚本执行完后,继续解析和渲染页面结构。看到这里,相信会有人问:浏览器为什么会这样处理,一溜执行下去不好吗?接下来上代码!!!

    ......
    
    <body>
        <p>
            <script type="text/javascript">
                document.write("The date is " + new Date().toDateString());
            </script>
        </p>
    </body>
    
    ......

对于上面的这段代码,当浏览器解析到 <p> 标签这里,发现里面有个叫 <script> 的标签,浏览器是不知道这个叫 <script> 的标签它葫芦里卖得什么药的(是否会对已经解析过得标签进行修改)。为了搞清楚这只葫芦里面的问题,浏览器必须停下来一心一意的处理这个葫芦,等这块的 JavaScript 脚本执行,然后继续往下进行解析和渲染。这样一方面保证了整个页面的完整性和统一性,另一方面也保证了整个页面的顺利解析。

走到这里,我们不禁会问,这个脚本的加载和解析竟然会这么麻烦,怎样才能最大程度的减少解析脚本的耗时,从而进行最大程度的优化呢?

脚本位置

说到这个脚本放置的位置,曾几何时,我也经常纠结于将 <script> 标签放在什么地方?

根据 HTML4 规范给出 <script> 标签可以放在 <head><body> 标签里面,并允许出现多次。一般认为将作为 JavaScript 脚本引入的 <script> 标签和作为 CSS 样式文件引入的 <link> 标签同时放在 <head> 标签里面,形如:

    <head>
        <script type="text/javascript" src="file1.js"></script>
        <script type="text/javascript" src="file2.js"></script>
        <link rel="stylesheet" type="text/css" href="file1.js"/>
    </head>

由于 JavaScript 脚本的阻塞特性,浏览器解析上面代码是按由上到下的顺序解析的。当解析到 <script> 标签时,浏览器会停止页面的渲染,一心一意对每个 <script> 标签进行下载、解析,这样一来用户的体验效果就会收到很明显的影响,更有可能出现页面显示空白的现象。

image

为此,IE8/Firefox 3.5/Safari 4/Chrome 2 都允许 JavaScript 文件的并行下载,但是在 JavaScript 文件下载的过程中仍然会阻塞其他资源的下载。不得已,推荐将所有的 <script> 标签尽可能的放到 <body> 标签的底部,以便尽量减少对整个页面渲染的影响,形如:

    ......
    
    <body>
        <!-- other html tags -->
        
        <script type="text/javascript" src="file1.js"></script>
        <script type="text/javascript" src="file2.js"></script>
    </body>
    
    ......

这样做的优点:1、因为浏览器解析、渲染页面是由上至下的顺序,让 JavaScript 脚本在 DOM 解析完之后下载并解析,保证了整体页面渲染的一致性和顺序性,避免了因 JavaScript 脚本中途解析导致页面显示得不正常和 DOM 节点获取不到的异常,2、避免了因 JavaScript 文件的阻塞特性,导致整体 HTML 渲染的性能问题。

组织脚本

由于每个 <script> 标签初始下载时都会阻塞页面渲染,所以减少页面包含的 <script> 标签数量有助于改善页面渲染性能问题。对于外链的 JavaScript 文件,HTTP 请求会存在额外的性能开销,因此下载一个 100KB 的文件比下载 425KB 的文件会更快。

接下来,通过各种无阻塞模式来优化脚本文件下载解析的性能问题。

延迟脚本

Defer 属性指明本元素所含有的脚本不会修改 DOM ,因此当浏览器解析到对应的 <script> 标签是开始下载 JavaScript 文件(此时的 JavaScript 文件的下载不会阻塞其他资源文件的下载),但并不会立即执行,直到 DOM 加载完成( onload 事件被触发前)。它不是一种跨浏览器的解决方案,因为该属性仅支持 IE 4+Firefox 3.5+ 的浏览器。

    <body>
       <script defer>
            alert('defer');
       </script>
       <script>
            alert('script');
       </script>
       <script>
            window.onload = function(){
                alert('load');
            }
       </script>
    </body>

上面的代码,在支持 defer 属性的浏览器中依次弹出 'script', 'defer', 'load',在不支持 defer 属性的浏览器中依次弹出 'defer', 'script', 'load'

动态脚本元素

动态脚本加载可能是现今比较流行的优化技术了。由于 JavaScript 脚本可以对 DOM 进行增删改查,那创建 <script> 标签进行脚本引入也就自然而然了。

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

这段脚本创建了一个 <script> 标签来加载 file1.js 文件。文件在该元素添加到页面时开始下载,并立即执行返回的脚本文件(除了 FirefoxOpera,他们会等待所有所有动态脚本节点执行完毕)。为了实现跨浏览器,我们将上面的脚本优化如下

    function loadScript(url){
        var script = document.createElement('script');
        script.type = 'text/javascript';
    
        if(script.readyState){  //IE
            //在 IE 浏览器中对 <script> 元素的接受会触发 readystatechange 事件
            // <script> 元素提供了 readystate 属性,根据不同阶段,取值分别为 
            // uninitialized => 初始化状态
            // loading => 开始下载
            // loaded => 下载完成
            // interactive => 数据完成下载但上不可用
            // complete => 所有数据已准备就绪
            script.onreadystatechange = function(){
                if(script.readystate == 'load' || script.readystate == 'complete'){
                    script.onreadystatechange = null;
                    callback();
                }
            }
        }else{  //其他浏览器
            script.onload = function(){
                callback()
            }
        }
        
        script.src = 'file1.js';
        document.getElementsByTagName('head')[0].appendChild(script);
    }

上面的方法调用脚本加载形式如下:

    loadScript('file1.js', function(){
        alert('file1 is loaded!');
        loadScript('file2.js', function(){
            alert('file2 is loaded!');
        })
    })

XMLHttpRequest 脚本注入

无阻塞脚本加载的方式是 XMLHttpRequest 脚本注入,此技术会创建一个 XHR 对象,然后用它下载 JavaScript 文件,最后通过创建动态的 <script> 元素将代码注入到页面当中。

    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);

代码发送一个 GET 请求获取 file1.js 源文件。事件处理函数 onreadystatechange 检查 readystate 是否为 4 ,同时校验 HTTP 状态码是否有效( 2XX 表示有效响应,304 标识从缓存读取 )。如果收到有效响应,就立即创建 <script> 元素,并设置该元素的 text 属性为从服务器端接收的 responseText,创建一个带内联脚本的 <script> 标签。

这样做的好处是,可以自主的控制脚本执行的时机。由于脚本内容是在 <script> 标签之外的,因此下载后不会立即执行。还有就是同样的代码几乎所有的浏览器都能正常工作。这种方法主要的局限性是,JavaScript 文件必须和所请求的页面处于相同的域。

管理浏览器中的 JavaScript 脚本是一个棘手的问题,由于 JavaScript 阻塞特性,通过以上的策略可以极大的提高需要大量使用 JavaScriptweb 应用的实际性能。