前端性能之非阻塞加载js脚本

来源:互联网 发布:大疆没有飞行数据 编辑:程序博客网 时间:2024/06/06 06:29

SCRIPT标签的阻塞行为会对页面的性能产生影响,这是因为浏览器在下载脚本、解析、执行的过程中不会同时做其他事情,比如渲染页面、响应用户事件等。之所以这样做是因为正在执行的JavaScript代码可能会改变页面元素修改样式添加或者删除事件等各种操作,以及最关键的脚本之间的依赖性,浏览器必须等待当前执行的脚本执行完成之后再进行后续操作。

脚本阻塞

两种加载方式

HTML页面中的JavaScript脚本有两种方式加入
- 使用script标签内联到HTML页面,页面按从上到下的顺序执行到script标签处时执行js代码,后续HTML内容会被阻塞。
- 使用script标签的src属性将js文件从外部加载,这需要浏览器查询对应文件的缓存,如果不可用就要重新进行http请求,这样就又会产生网络延迟和下载后的解析与执行,这都会阻塞页面。

浏览器特性

一般情况下,大多数浏览器支持并行下载html、css、图片等元素,不过对于同一个域名下的资源,浏览器默认最多并行下载的个数有限制,一般是4个。另外,对于JavaScript脚本,浏览器却不支持并行下载,当下载一个脚本并解析、执行后,才能执行后一个。
浏览器之所以保证不能并行下载多个脚本,是为了防止两个有依赖的脚本在浏览器的执行顺序颠倒后,会引发变量、函数未定义的错误。同时也会更改html内容。因此,脚本必须要顺序地执行,但是这并不代表它们必须要顺序地下载。
IE8是第一个支持并行下载JavaScript脚本的浏览器,这使得页面加载多个脚本的速度加快了很多,但是这并没有解决脚本执行的阻塞问题,当A.js和B.js并行下载之后,它们依然要进行解析和顺序执行,这段时间必须要阻塞浏览器的其他行为,也就是等这段时间过了之后才能下载后续图片、css、iframe等元素。Chrome 2+和Safari 4+浏览器也是和IE8类似,会并行下载脚本但是不能
因此,最终的目的是要并行下载脚本的同时,也能下载其他元素而不会有阻塞的问题。这里都以外部脚本文件的加载来说明。

非阻塞加载外部脚本

为了避免脚本的阻塞问题,最简单的方式就是讲所有JavaScript脚本内联到HTML中,将脚本放在所有可显示元素最后面,就可以避免这个问题,但是对于大型的js文件以及缓存js文件的考虑,这个问题需要进行折中处理。针对外部的js文件请求,主要有如下几个方式进行

XHR Eval

使用XMLHTTPRequest对象从服务器异步获取js脚本文件,当响应完成后使用JavaScript语言的eval函数对响应内容进行执行。
优点:异步请求的js文件不会阻塞其他元素(图片、css等)的下载,脚本异步下载完成之后就执行。浏览器不显示”等待“。
缺点:请求的js文件必须要与主页面在同一个域名下,这对于CDN或者多域名的处理不便。不能保证执行顺序。

var xhrObj = getXHRObject();xhrObj.onreadystatechange = function(){    if(xhrObj.readyState == 4 && xhrObj.status == 200){        //依赖文件队列处理        eval(xhrObj.responseText);        //后续处理    }};xhrObj.open('GET', 'a.js', true);xhrObj.send('');

一般为了保证异步加载的js文件的依赖性,需要手动保存好依赖文件队列,使其按依赖顺序执行。

XHR Injection

与XHR Eval类似,XHR Injection将异步获取的内容使用动态创建script标签的形式插入到DOM元素中去。实际测试显示使用eval方法会比这种方法的速度慢一些。

var xhrObj = getXHRObject();xhrObj.onreadystatechange = function(){    if(xhrObj.readyState == 4 && xhrObj.status == 200){        //依赖文件队列处理        var script = document.createElement('script');        document.getElementsByTagName('head')[0].appendChild(script);        script.text = xhrObj.responseText;        //后续处理    }};xhrObj.open('GET', 'a.js', true);xhrObj.send('');

这种方法的优缺点与XHR Eval差不多,但是速度可能会快一些。

Script in Iframe

iframe可以与主页面的其他元素并行下载,并且不会阻塞。但是iframe是为了包含其他的HTML页面,其他的HTML页面也可以包含js脚本,因此可以将需要加载的js脚本放入一个html文件中然后使用iframe非阻塞加载这个html文件即可。
优点:异步加载脚本,浏览器支持较好
缺点:需要与主页面同一个域名,需要将外部的js文件转换为html从而作为iframe的src属性。另外,iframe是一个非常重量级的DOM元素。不能保证执行顺序。浏览器会显示”等待“。
iframe本身也都可以使用js脚本进行动态创建:

var _ = function(d){document.getElementById(d);};var removeNode = function(a) {   try {      typeof a == "string" && (a = _(a));      a.parentNode.removeChild(a)   } catch (b) {}};var addEvent = function(ele, event, call){    ele.addEventListener ? ele.addEventListener(event, call, !1) : a.attachEvent ? a.attachEvent("on" + event, call) : a["on" + event] = call;};var loadScriptByIframe = function(id, src){    src == null && (src = "javascript:false;");    removeNode(id);    var c = document.createElement('iframe');    c.height = 0;    c.width = 0;    c.style.display = "none";    c.name = id;    c.id = id;    c.src = src;    c.isReady = !1;    addEvent(c, "load", function(){        if(! c.isReady){            c.isReady = !0;            //当前脚本已加载完成,执行其他js代码        }    });    document.body.appendChild(c);    window.frames[id].name = id;    return c;};

在iframe和主页面中的脚本需要进行修改才能互相访问:
- 从主页面中访问iframe

window.frames[0].methodInIframe(); //使用framesdocument.getElementById('iframeID').contentWindow.methodInIframe(); //使用getElementById
  • 从iframe访问主页面
function methodInIframe(){    var newDiv = parent.document.createElement('div');    parent.document.body.appendChild(newDiv);}

Script DOM Element

这个方法是使用最多的方法,通过js脚本动态创建script标签插入到DOM中,可以动态设置src属性,并且可以是不同域名下的js文件,一般百度统计、cnzz、google analysis等网站统计工具提供的统计代码就是使用这种方式,是最为人知使用最为广泛的方法。
优点:可以跨域,创建script元素不会阻塞其他元素的下载,并且代码实现简单方便。
缺点:下载后的脚本并不能保证按顺序执行,仅仅在FireFox浏览器下保证顺序执行,其他浏览器存在脚本文件的依赖冲突。浏览器显示”等待“。

var loadScriptByScriptDom(id, src, c){    removeNode(id);    var d = document.getElementsByTagName('head')[0],        e = document.createElement('script');    e.charset = c || 'utf-8';    e.id = id;    e.type = 'text/javascript';    e.src = src;    d.appendChild(e);}

Script Defer

IE浏览器的script标签支持defer属性,当使用这个属性之后,浏览器会识别,表示不会立即下载这个script标签对于的js文件,当这个js文件中没有document.write调用,并且没有其他脚本依赖这个文件,那么就可以使用。IE在下载这个文件时不会阻塞其他元素的下载。
优点:实现方便,保证js文件执行的顺序,支持跨域
缺点:浏览器会显示”等待”状态,仅仅支持defer属性的IE浏览器适用。

<script defer src="a.js"></script>

document.write Script tag

JavaScript语言中可以调用document的write方法将内容写入到html的DOM中,这种方式仅仅在IE中并行下载,并且在下载的过程中其他资源依然会被阻塞。并不是一种好的解决方式。

document.write("<script type='text/javascript' src='a.js'></script>")

评价标准

浏览器”等待“标志

浏览器等待的标志包括:状态栏、进度条、tab图标、光标形状,另外还有阻塞的渲染和阻塞的onload事件,前者是在当前下载的js脚本之后的可视内容都被阻塞从而不会渲染,后者是只有所有资源都下载完成呈现页面之后这个事件才会触发。
绝大多数浏览器的”等待”状态都会被使用script的src方式加载的过程中被触发,因此会被用户感知到页面还未完成加载。但是这些状态标识不会被基于XHR的XHR Eval和XHR Injection方法触发。

多个js脚本的执行顺序

当使用上述的方法加载多个js脚本时,如果他们之间有依赖关系,那么必须保证其执行顺序不变,否则会出现各种未定义的错误。大多数情况下,这不仅与方法有关,也与浏览器有关,使用script 的src属性的方法都保证执行顺序与页面中排布的顺序一致。
对于IE浏览器,script defer和document.write script tag方法保证其执行顺序与排布顺序一致。
FireFox浏览器,不支持script defer属性,同时document.write方法也不能进行并行下载。但是script DOM方法可以在FireFox浏览器中按照其在页面中的排布顺序来执行,但是其他浏览器却不行。
上述基于XHR的方法是不能保证执行的顺序的,但是可以通过下载队列的手动处理后来保证其执行顺序。

结论

根据上述多方面的评价,document.write方法不仅对浏览器的依赖很严重,自身也依然会阻塞后续资源,因此应该避免使用,其他方式针对不同浏览器情况,以及是否需要保证执行顺序和是否显示浏览器的“等待”标志,需要区别对待才行。最终版本如下:

OS = function(){    var dom = document,        me = this;    this.addEvent = function(ele, event, call){        ele.addEventListener ? ele.addEventListener(event, call, !1) : a.attachEvent ? a.attachEvent("on" + event, call) : a["on" + event] = call;    };    this.getXHRObject = function(){        var xhrObj = false;        try{            xhrObj = new XMLHttpRequest();        }catch(e){            var msTypes = ["Msxml2.XMLHTTP.6.0",                           "Msxml2.XMLHTTP.3.0",                           "Msxml2.XMLHTTP",                           "Microsoft.XMLHTTP"];            var l = msTypes.length;            for(var i = 0; i < l; i++){                try{                    xhrObj = new ActiveXObject(msTypes[i]);                }catch(e){                    continue;                }                break;            }        }finally{            return xhrObj;        }    };    this.LoadScript = {        Default : function(url, onload){            me.LoadScript .DomElement(url, onload);        },        DomElement : function(url, onload){            var domscript = dom.createElement('script');            domscript.src = url;            if (onload){                domscript.onloadDone = false;                domscript.onload = onload;                domscript.onreadystatechange = function(){                    if ("loaded" === domscript.readyState && domscript.onloadDone){                        domscript.onloadDone = true;                        domscript.onload();                    }                }            }            dom.getElementsByTagName('head')[0].appendChild(domscript);        },        DocWrite : function(url, onload){            document.write('<scr' + 'ipt src="' + url +                '" type=text/javascript"></scr' + 'ipt>');            if (onload){                me.addEvent(window, "load", onload);            }        },        queuedScripts : new Array(),        XhrInjection : function(url, onload, isOredered){            var q = me.LoadScript.queuedScripts.length;            if (isOredered){                var qScript = {response: null, onload:onload, done: false};                me.LoadScript.queuedScripts[q] = qScript;            }            var xhrObj = me.getXHRObject();            xhrObj.onreadystatechange = function(){                if (xhrObj.readyState == 4){                    if (isOrdered){                            me.LoadScript.queuedScripts[q].response =                         xhrObj.responseText;                         me.LoadScript.injectScripts();                    }else{                        eval(xhrObj.responseText);                        if(onload) onload();                    }                }            };            xhrObj.open('GET', url, true);            xhrObj.send('');        },        injectScripts : function(){            var queue = me.LoadScript.queuedScripts;            var len = queue.length;            for (var i = 0; i < len; i++){                var qScript = queue[i];                if( ! qScript.done ){                    break;                }else{                    eval(qScript.response);                    if (qScript.onload){                        qScript.onload();                    }                    qScript.done = true;                }            }        },    };};//end of OS
0 0
原创粉丝点击