码迷,mamicode.com
首页 > Web开发 > 详细

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

时间:2015-07-31 20:22:41      阅读:128      评论:0      收藏:0      [点我收藏+]

标签:非阻塞js   前端   性能优化   

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(); //使用frames
document.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

版权声明:本文为博主原创文章,未经博主允许不得转载。

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

标签:非阻塞js   前端   性能优化   

原文地址:http://blog.csdn.net/u010487568/article/details/47174203

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!