前端性能优化成神之路-异步加载

浏览器渲染阻塞与优化

什么是阻塞:在页面中我们通常会引用外部文件,而浏览器在解析HTML页面是从上到下依次解析、渲染,如果<head>中引用了一个a.js文件,而这个文件很大或者有问题,需要2秒加载,那么浏览器会停止渲染页面(此时是白屏显示,就是页面啥都没有),2秒后加载完成才会继续渲染,这个就是阻塞。
 
为什么会阻塞:因为浏览器不知道a.js中执行了哪些脚本,会对页面造成什么影响,所以浏览器会等js文件下载并执行完成后才继续渲染,如果这个时间过长,会白屏。
CSS文件也一样,因为CSS文件会对DOM的样式,布局,色彩,效果产生影响,所以浏览器会等CSS文件下载并执行完成后继续。为了页面的性能,要避免阻塞。
 

 

 

异步加载的方式1:动态脚本加载

JavaScript 倾向于阻塞浏览器某些处理过程,如 HTTP 请求和界面刷新,这是开发者面临的最显著的性能问题。保持 JavaScript 文件短小,并限制 HTTP 请求的数量,只是创建反应迅速的网页应用的第一步。一个应用程序所包含的功能越多,所需要的 JavaScript 代码就越大,保持源码短小并不总是一种选择。尽管下载一个大 JavaScript 文件只产生一次 HTTP 请求,却会锁定浏览器一大段时间。为避开这种情况,你需要向页面中逐步添加 JavaScript,某种程度上说不会阻塞浏览器。

非阻塞脚本的秘密在于,等页面完成加载之后,再加载 JavaScript 源码。从技术角度讲,这意味着在,window 的 load 事件发出之后开始下载代码这里重点说一下Dynamic Script Elements 动态脚本元素
文档对象模型(DOM)允许你使用 JavaScript 动态创建 HTML 的几乎全部文档内容。其根本在于,<script>元素与页面其他元素没有什么不同:引用变量可以通过 DOM 进行检索,可以从文档中移动、删除,也可以被创建。一个新的<script>元素可以非常容易地通过标准 DOM 函数创建
var script = document.createElement ("script");
script.type = "text/javascript";
script.src = "file1.js";
document.getElementsByTagName_r("head")[0].appendChild(script);
新的<script>元素加载 file1.js 源文件。此文件当元素添加到页面之后立刻开始下载。此技术的重点在于:
无论在何处启动下载,文件的下载和运行都不会阻塞其他页面处理过程。你甚至可以将这些代码放在<head>部分而不会对其余部分的页面代码造成影响(除了用于下载文件的 HTTP 连接)
在<script>元素的生命周期中,readyState 的这些取值不一定全部出现,但并没有指出哪些取值总会被用到。实践中,我们最感兴趣的是“loaded”和“complete”状态。Internet Explorer 对这两个
readyState 值所表示的最终状态并不一致,有时<script>元素会得到“loader”却从不出现“complete”,但另外一些情况下出现“complete”而用不到“loaded”。最安全的办法就是在 readystatechange 事件中检查这两种状态,并且当其中一种状态出现时,删除 readystatechange 事件句柄(保证事件不会被处理两次):
 
调用一个函数就可以实现 JavaScript 文件的动态加载。下面的函数封装了标准实现和 IE 实现所需的功能,此函数接收两个参数:JavaScript 文件的 URL,和一个当 JavaScript 接收完成时触发的回调函数。属性检查用于决定监视哪种事件。最后一步,设置 src 属性,并将<script>元素添加至页面。
创建一个file.js文件

然后再页面中通过动态创建script标签的方式加载这个js文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div class="">
        test
        <script type="text/javascript">
            console.log('write');
            document.write('<span>write</span>');
        </script>
        <script type="text/javascript">
            for (var i = 0; i < 200000; i++) {
                if (i % 20000 === 0) {
                    console.log(i);
                }
            }
        </script>
        <script>
            function loadScript(url, callback){
                var script = document.createElement("script")
                script.type = "text/javascript";
                if (script.readyState){ //IE
                    script.onreadystatechange = function(){
                        if (script.readyState == "loaded" || script.readyState == "complete"){
                            script.onreadystatechange = null;
                            callback();
                        }
                    };
                } else { //Others
                    script.onload = function(){
                        callback();
                    };
                }
                script.src = url;
                document.getElementsByTagName("head")[0].appendChild(script);
            }

            loadScript("file1.js", function(){
                alert("File is loaded!");
            });
        </script>
    </div>
</body>
</html>
通过浏览器测试可以看到,这个js文件一被加载就执行了
你可以在页面中动态加载很多 JavaScript 文件,但要注意,浏览器不保证文件加载的顺序。所有主流浏览器之中,只有 Firefox 和 Opera 保证脚本按照你指定的顺序执行。其他浏览器将按照服务器返回它们的次序下载并运行不同的代码文件。你可以将下载操作串联在一起以保证他们的次序
            loadScript("file1.js", function(){
                loadScript("file2.js", function(){
                    loadScript("file3.js", function(){
                        alert("All files are loaded!");
                    });
                });
            });
此代码等待 file1.js 可用之后才开始加载 file2.js,等 file2.js 可用之后才开始加载 file3.js。虽然此方法可行,但如果要下载和执行的文件很多,还是有些麻烦。如果多个文件的次序十分重要,更好的办法是将这些文件按照正确的次序连接成一个文件。独立文件可以一次性下载所有代码(由于这是异步进行的,使用一个大文件并没有什么损失)。
 
 
 
 
异步加载的方式2:用 XHR 对象下载代码,并注入到页面中
实现原理:XMLHttpRequest Script Injection XHR另一个以非阻塞方式获得脚本的方法是使用 XMLHttpRequest(XHR)对象将脚本注入到页面中。此技术首先创建一个 XHR 对象,然后下载 JavaScript 文件,接着用一个动态<script>元素将 JavaScript 代码注入页面。
执行原理:此代码向服务器发送一个获取 file1.js 文件的 GET 请求。onreadystatechange 事件处理函数检查 readyState是不是 4,然后检查 HTTP 状态码是不是有效(2XX 表示有效的回应,304 表示一个缓存响应)。如果收到了一个有效的响应,那么就创建一个新的<script>元素,将它的文本属性设置为从服务器接收到的responseText 字符串。这样做实际上会创建一个带有内联代码的<script>元素。一旦新<script>元素被添加到文档,代码将被执行,并准备使用。
这种方式的优点:这种方法的主要优点是,你可以下载不立即执行的 JavaScript 代码。由于代码返回在<script>标签之外(换句话说不受<script>标签约束),它下载后不会自动执行,这使得你可以推迟执行,直到一切都准备好了。另一个优点是,同样的代码在所有现代浏览器中都不会引发异常。此方法最主要的限制是:JavaScript 文件必须与页面放置在同一个域内,不能从 CDNs 下载(CDN 指“内容投递网络(Content Delivery Network)”。正因为这个原因,大型网页通常不采用 XHR 脚本注入技术。 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div class="">
        test
        <script type="text/javascript">
            console.log('write');
            document.write('<span>write</span>');
        </script>
        <script type="text/javascript">
            for (var i = 0; i < 200000; i++) {
                if (i % 20000 === 0) {
                    console.log(i);
                }
            }
        </script>
        <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);
        </script>
    </div>
</body>
</html>

 执行的效果如下:

 

 

 

异步加载方式3:defer

HTML 4 为script标签定义了一个扩展属性:defer。

defer是在HTML解析完成之后就会执行,如果有多个,按照加载的顺序依次执行

Defer 属性指明本元素所含的脚本不会修改 DOM,因此代码能安全地延迟执行。defer 属性只被 IE 4 和 Firefox 3.5 更高版本的浏览器所支持,所以它不是一个理想的跨浏览器解决方案。在其他浏览器中,defer 属性会被直接忽略,因此script标签会以默认的方式处理,也就是说会造成阻塞。然而,如果您的目标浏览器支持的话,这仍然是个有用的解决方案。使用方式如下:

<script type="text/javascript" src="script1.js" defer></script>

带有 defer 属性的script标签可以放置在文档的任何位置。对应的 JavaScript 文件将在页面解析到script标签时开始下载,但不会执行,直到 DOM 加载完成,即onload事件触发前才会被执行。当一个带有 defer 属性的 JavaScript 文件下载时,它不会阻塞浏览器的其他进程,因此这类文件可以与其他资源文件一起并行下载

实例演示:

创建两个js分别为defer1.js和defer2.js

然后在页面中进行加载

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="defer1.js" charset="utf-8" defer></script>
    <script src="defer2.js" charset="utf-8" defer></script>
</head>
<body>
    <div class="">
        test
        <script type="text/javascript">
            console.log('write');
            document.write('<span>write</span>');
        </script>
        <script type="text/javascript">
            for (var i = 0; i < 200000; i++) {
                if (i % 20000 === 0) {
                    console.log(i);
                }
            }
        </script>
    </div>
</body>
</html>

运行结果:

可以看到页面中两段js代码先执行(同步的方式),后执行了异步加载的两个js文件的代码

 

 

异步加载方式4:async

HTML 5 为script标签定义了一个新的扩展属性:async

它的作用和 defer 一样,能够异步地加载和执行脚本,不因为加载脚本而阻塞页面的渲染。但是有一点需要注意,在有 async 的情况下,JavaScript 脚本一旦下载好了就会执行所以很有可能不是按照原本的顺序来执行的。如果 JavaScript 脚本前后有依赖性,使用 async 就很有可能出现错误。

演示实例:

创建两个js文件

然后再页面中进行加载

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="async1.js" charset="utf-8" async></script>
    <script src="async2.js" charset="utf-8" async></script>
</head>
<body>
    <div class="">
        test
        <script type="text/javascript">
            console.log('write');
            document.write('<span>write</span>');
        </script>
        <script type="text/javascript">
            for (var i = 0; i < 200000; i++) {
                if (i % 20000 === 0) {
                    console.log(i);
                }
            }
        </script>
    </div>
</body>
</html>

运行结果如下:

当两个js文件的代码量比较大的时候,就有可能出现先执行async2.js这个文件的代码

posted @ 2017-08-17 02:31  胡椒粉hjf  阅读(1245)  评论(0编辑  收藏  举报