stay hungry stay foolish

前端性能优化之-js异步加载

默认情况JavaScript是同步加载的,也就是javascript的加载是阻塞的,后面的元素要等待javascript加载完毕后才能进行再加载,对于一些意义不是很大的javascript,如果放在页头会导致加载很慢的话,是会严重影响用户体验的。

因此有时候有必要实现js的异步加载,js异步主要有三种方式:defer(内联模式下仅IE可用),async(HTML5新属性),用js创建script,接下来,我们就来一起来看看这三种方式的使用。

defer

defer的官方定义:

defer 属性规定是否对脚本执行进行延迟,直到页面加载为止。如果您的脚本不会改变文档的内容,可将 defer 属性加入到 <script> 标签中,以便加快处理文档的速度。因为浏览器知道它将能够安全地读取文档的剩余部分而不用执行脚本,它将推迟对脚本的解释,直到文档已经显示给用户为止。

示例:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>Document</title>  
    <script type="text/javascript">  
        window.onload=function(){  
            console.log("页面加载完成");  
        }  
        document.addEventListener('DOMContentLoaded',function(){  
            console.log("文档元素加载完成");  
        },false);  
    </script>  
    <script type="text/javascript"  defer="defer">  
        console.log("有defer属性的脚本内容");  
        console.log(document.getElementById("p1").innerHTML);  
    </script>  
    <script type="text/javascript">  
        console.log("没有defer属性的脚本内容");  
    </script>  
</head>  
<body>  
    <p id="p1">测试内容</p>  
</body>  
</html>  

IE8下的测试结果:*

通过输出的日志可以看到,文档会先解析页面,等页面解析完成后才去执行defer属性标记的脚步内容,最后再触发window的onload(页面的文档结构和图片都加载完成后触发)事件,注意:IE8不支持DOMContentLoad(页面文档结构解析完成)事件。

360浏览器测试结果:

可以看到,360浏览器并不支持defer属性,注意:DOMContentLoad事件先于onload事件。

不过对于外部js文件,defer在各浏览器上都是支持的

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>Document</title>  
    <script type="text/javascript" defer="defer" src="test.js">
    </script>
    <script type="text/javascript">
    	console.log(2)
    </script>
</head>  
<body>  
</body>  
</html> 

test.js:

console.log('out:1');

测试结果:

async

async官方解释:

async 属性规定一旦脚本可用,则会异步执行。

注释:async 属性仅适用于外部脚本(只有在使用 src 属性时)。

注释:有多种执行外部脚本的方法:

  • 如果 async=”async”:脚本相对于页面的其余部分异步地执行(当页面继续进行解析时,脚本将被执行,也就是脚本的下载和页面的解析异步执行,脚本一下载完就立即执行)
  • 如果不使用 async 且 defer=”defer”:脚本将在页面完成解析时执行
  • 如果既不使用 async 也不使用 defer:在浏览器继续解析页面之前,立即读取并执行脚本

示例:

async.html:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>Document</title>  
    <script type="text/javascript">  
        window.onload=function(){  
            console.log("页面加载完成");  
        }  
        document.addEventListener('DOMContentLoaded',function(){  
            console.log("文档元素加载完成");  
        },false);  
    </script>  
    <script type="text/javascript" src="test.js" async="async">  
    </script>  
</head>  
<body>  
    <script type="text/javascript">  
        console.log("没有async属性的脚本内容");  
        setTimeout(function(){  
            var p = document.createElement("p");  
            p.innerHTML = "动态创建的内容";  
            document.body.appendChild(p);   
        },600)  
    </script>  
</body>  
</html>  

test.js:

console.log("有async属性的脚本内容");  
console.log(document.getElementsByTagName("p")[0].innerHTML);  

tomcat下用360浏览器测试结果:

注意图中红线框中的内容,设置网络延迟500ms以便测试,此时test.js的下载和运行耗时总共为676,因为我们在用js动态创建p标签时设置了延时为600ms,当test.js执行时,p标签已经动态生成了,所以可以获取到p标签,但是当我们用js动态创建p标签设置延时为700ms(大于test.js的下载和运行耗时)

<body>  
    <script type="text/javascript">  
        console.log("没有async属性的脚本内容");  
        setTimeout(function(){  
            var p = document.createElement("p");  
            p.innerHTML = "动态创建的内容";  
            document.body.appendChild(p);   
        },700)  
    </script>  
</body> 

其结果如下:

此时,test.js运行时是获取不到动态生成的p标签的,因为其下载和运行耗时为663ms,小于我们设置的延时700ms。async属性标记的外部脚本在DOMContentLoaded事件之后onLoad事件之前执行。

以下是一张图(引用自https://segmentfault.com/q/1010000000640869)

蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

此图告诉我们以下几个要点:

  • defer 和 async 在网络读取(下载)这块儿是一样的,都是异步的(相较于 HTML 解析)
  • 它俩的差别在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的
  • 关于 defer,此图未尽之处在于它是按照加载顺序执行脚本的,这一点要善加利用
  • async 则是一个乱序执行的主,反正对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行

仔细想想,async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的,最典型的例子:Google Analytics

动态创建script标签

示例:

async.html:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>Document</title>  
    <script type="text/javascript">  
        window.onload=function(){  
            console.log("页面加载完成");  
        }  
        document.addEventListener('DOMContentLoaded',function(){  
            console.log("文档元素加载完成");  
        },false);  
    </script>  
    <script type="text/javascript">  
        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: Firefox, Safari, Chrome, and Opera   
                script.onload = function(){   
                    callback();   
                };  
            }   
            script.src = url;   
            document.getElementsByTagName("head")[0].appendChild(script);   
        }  
        loadScript("test.js",function(){});  
    </script>  
</head>  
<body>  
    <script type="text/javascript">  
        setTimeout(function(){  
            var p = document.createElement("p");  
            p.innerHTML = "动态创建的内容";  
            document.body.appendChild(p);  
        },600);  
          
    </script>  
</body>  
</html>  

test.js:

console.log("js加载的脚本内容");  
console.log(document.getElementsByTagName("p")[0].innerHTML);  

360浏览器测试结果:

把延迟改成700ms:

<body>  
    <script type="text/javascript">  
        setTimeout(function(){  
            var p = document.createElement("p");  
            p.innerHTML = "动态创建的内容";  
            document.body.appendChild(p);  
        },700);  
          
    </script>  
</body> 

测试结果:

通过这个例子可以看出,用js动态创建script的方式来加载js脚本,其效果和async属性是一样的。