资讯专栏INFORMATION COLUMN

js文件加载优化

zhaochunqi / 2433人阅读

摘要:所以这里需要另外的操作来对文件加载进行优化加载这是中定义的一个属性,它用来表示的是,当渲染引擎遇到的时候,如果引用的是外部资源,则会暂时挂起,并进行加载。

在js引擎部分,我们可以了解到,当渲染引擎解析到script标签时,会将控制权给JS引擎,如果script加载的是外部资源,则需要等待下载完后才能执行。 所以,在这里,我们可以对其进行很多优化工作。

放置在body底部

为了让渲染引擎能够及早的将DOM树给渲染出来,我们需要将script放在body的底部,让页面尽早脱离白屏的现象,即会提早触发DOMContentLoaded事件. 但是由于在IOS Safari, Android browser以及IOS webview里面即使你把js脚本放到body尾部,结果还是一样。 所以这里需要另外的操作来对js文件加载进行优化.

defer加载

这是HTML4中定义的一个script属性,它用来表示的是,当渲染引擎遇到script的时候,如果script引用的是外部资源,则会暂时挂起,并进行加载。 渲染引擎继续解析下面的HTML文档,解析完时,则会执行script里面的脚本。

他的支持度是<=IE9的.
并且,他的执行顺序,是严格依赖的,即:


当页面解析完后,他便会开始按照顺序执行 outside1 和 outside2文件。
如果你在IE9以下使用defer的话,可能会遇到 它们两个不是顺序执行的,这里需要一个hack进行处理,即在两个中间加上一个空的script标签


 //hack

但是,如果你将defer属性用在inline的script脚本里面,在Chrome和FF下是没有效果的。
即:

async加载

async是H5新定义的一个script 属性。 他是另外一种js的加载模式。

渲染引擎解析文件,如果遇到script(with async)

继续解析剩下的文件,同时并行加载script的外部资源

当script加载完成之后,则浏览器暂停解析文档,将权限交给JS引擎,指定加载的脚本。

执行完后,则恢复浏览器解析脚本

可以看出async也可以解决 阻塞加载 这个问题。不过,async执行的时候是异步执行,造成的是,执行文件的顺序不一致。即:


这时,谁先加载完,就先执行谁。所以,一般依赖文件就不应该使用async而应该使用defer.
defer的兼容性比较差,为IE9+,不过一般是在移动端使用,也就不存在这个problem了。
其实,defer和async的原理图,如图一样。(包括放在head中的script标签)

脚本异步

脚本异步是一些异步加载库(比如require)使用的基本加载原理. 直接上代码:

function asyncAdd(src){
    var script = document.createElement("script");
    script.src = src;
    document.head.appendChild(script);
}
//加载js文件
asyncAdd("test.js");

这时候,可以异步加载文件,不会造成阻塞的效果.
但是,这样加载的js文件是无序的,无法正常加载依赖文件。
如果你想要js文件按照你自定义的顺序执行,则要将async设置为false. 但是会阻塞其它文件的加载

var asyncAdd = (function(){
    var head = document.head,
        script;
    return function(src){
        script = document.createElement("script");
        script.src= src;
        script.async=false;
        document.head.appendChild(script);
    }
})();
//加载文件
asyncAdd("first.js");
asyncAdd("second.js");
//或者简便一点
["first.js","second.js"].forEach((src)=>{async(src);});

但是,使用脚本异步加载的话,需要等待css文件加载完后,才开始进行加载,不能充分利用浏览器的并发加载优势。而使用静态文本加载async或者defer则不会出现这个问题。
使用脚本异步加载时,只能等待css加载完后才会加载

使用静态的async加载时,css和js会并发一起加载

(from 妙净)

关于这三种如何取舍,那就主要看leader给我们目标是什么,是兼容IE8,9还是手机端,还是桌面浏览器,或者两两组合。
但是对于多带带使用某一个技能的场景,使用时需要注意一些tips。
js文件放置位置应该放置到body末尾
如果使用async的话,最后加上defer以求向下兼容

 //如果两者都支持,async会默认覆盖掉defer
//如果只支持一个,则执行对应的即可

通常,我们使用的加载都是defer加载(因为很强的依赖关系).
但,上面的简单js文件依赖加载只针对于,依赖关系不强,或者说,相互关联性不强的js文件。先在js模块化思想 已经成为主流, 如果这样手动添加defer或者async是没有太大的实际意义的。
原因就在于, 好复杂~
所以,才有了webpack,requireJS等模块打包工具。这也是给我们在性能和结构上寻找一个平衡点的尝试。
这里也给大家安利一些建议:
业务逻辑代码使用模块化书写, 测试代码或者监听代码使用async,或者defer填充。 这也是比较好的实践。

深入脚本异步加载

最简单的脚本异步就是在head里添加一个script标签.

var asyncAdd = (function(){
    var head = document.head,
        script;
    return function(src){
        script = document.createElement("script");
        script.async=false;
        document.head.appendChild(script);
    }
})();
asyncAdd("test.js"); //异步加载文档

这样写,其实还不如,直接加async. 这样简单的异步加载,是不能满足我们模块化书写的庞大业务逻辑的。 这里,我们将一步一步的优化我们的代码,实现,异步js文件加载的模块化.

串行加载js文件

对上述简单js异步脚本的升级版就是使用串行方式,加载js脚本。首先,我们需要了解一下,DOMreadyState和onload事件,这里先安利一下Nicholas大神 推荐的一份检测onload的脚本:

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.body.appendChild(script);
}

但从IE11开始,已经支持onload事件, 不过,现在这份代码的价值还是非常大的, 目前主流兼容IE8+。
当然,我们可以使用loadScript中进行回调加载.

loadScript("test1.js",loadScript("test2.js",loadScript("test3.js")));

不过,这简直就是没人性的写法。 所以,这里我们可以进行优化一下。我们可以使用以前的模式,进行重构,这里我选择命令模式和链式调用。
直接贴代码吧:

 var loadJs = (function() {
        var script = document.createElement("script");
        if (script.readyState) {
            return function(url, cb) {
                script = document.createElement("script");
                script.src = url;
                document.body.appendChild(script);
                script.onreadystatechange = function() {
                    if (script.readyState == "loaded" ||
                        script.readyState == "complete") {
                        script.onreadystatechange = null; //解除引用
                        cb();
                    }
                };
            }
        } else {
            return function(url, cb) {
                script = document.createElement("script");
                script.src = url;
                document.body.appendChild(script);
                script.onload = function() {
                    cb();
                };
            }
        }
    })();

    //测试用例: commandJs.add("test.js",[test.js,test1.js]).exe();
    //或者 commandJs.add("test.js").add("test1.js").add([test1.js,test2,js]).exe();
    var commandJs = (function() {
        var group = [],
            len = 0;
        //类型检测
        //数组
        var isArray = function(para) {
                return (para instanceof Array);
            }
            //String类型
        var isString = function(para) {
                return Object.prototype.toString.call(para) === "[object String]";
            }
            //集合检测
        var correctType = function(para) {
                return isString(para) || isArray(para);
            }
            //添加src内容
        var add = function() {
            for (var i = 0, js; js = arguments[i++];) {
                if (!correctType(js)) {
                    throw new Error(`the ${i}th js file"s type is not correct`);
                }
                group.push(js);
            }
            return this;
        }
        var isFinish = function() {
                len--;
                if (len === 0) {
                    exe(); //开始加载下一组js文件
                }
            }
            //并行加载js文件
        var loadArray = function(urls) {
            urls.forEach((url) => {
                loadJs(url, (function() {
                    isFinish(); //判断是否执行完全
                }).bind(this));
            });
        }
        var exe = function() {
            if (group.length === 0) return; //遍历完所有的urls时,退出执行
            var js = group.shift();
            if (isArray(js)) {
                len = js.length;
                loadArray(js);
            } else {
                len = 1;
                loadArray([js]);
            }
            return this;
        }
        return {
            exe,
            add
        }
    })();

OK, 我们来验证一样,串行执行的测试结果:

commandJs.add("./js/loader01.js").add("./js/loader02.js").exe();
//或者
commandJs.add("./js/loader01.js","./js/loader02.js").exe();
//这两种写法都是可以的

最后的结果是:

ok~ 可以通过,这样可以自定义加载很多依赖文件。 但是,造成结果是,时间成本耗费太大。 有时候, 一个主文件的main 有很多依赖js模块, 那么我们考虑一下,能否把这些js模块并行加载进来呢?

其实,上面的那一串代码,已经将串行和并行给结合起来了。那并行是怎么做的呢? 其实就是,同时向页面中添加script tag然后监听,是否所有的tag都已经加载完整。如果是,开始加载下一组js文件。

其实,最主要的代码块是这里:

  var isFinish = function() {
                len--;
                if (len === 0) {
                    exe(); //开始加载下一组js文件
                }
            }
            //并行加载js文件
        var loadArray = function(urls) {
            urls.forEach((url) => {
                loadJs(url, (function() {
                    isFinish(); //判断是否执行完全
                }).bind(this));
            });
        }
//执行顺序就是,然后中间加了一些trick进行,类型的判断.
//exe=> loadArray => isFinish ~>exe
并行加载js

OK, 上面我们已经测试了js的异步加载,这里我们测试一下js并行加载的效果:

commandJs.add(["./js/loader01.js","./js/loader02.js"]).exe();

上图时间:

我们对比一下异步加载的:

从上面很容易知道,异步和同步加载的区别,因为这个文件较小体现的价值不是很大,我们换一个比较大的文件进行加载:

//并行:
 commandJs.add(["http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js","https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-dom-server.js"]).exe();
 //串行
commandJs.add("http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js","https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-dom-server.js").exe();

看一下图:
//并行

//串行

大家可以钩钩手指,算一下两者的时间差, 一个是取max, 一个是取add. 结果是显而易见的。 当然,模块加载插件比如requireJS,labJS,他们所要做的功能比这里的要丰满的多, 当你 多个文件引入同一个依赖的时候,只需要加载一次(判断唯一性), 以及引用模块的ID 的 标识等。
js 脚本异步加载还有很多方法,比如xhr, iframe ,以及使用img 的 src进行加载,这些都是可行的, 但是他们的局限性也很大, xhr,iframe的同域要求,使用img还不如直接使用script。 我这里列一下他们的大概情况表吧

加载方式 实现效果
xhr 脚本并行下载,要求同域,不会阻塞其他资源
iframe 要求同域,脚本并行下载,不阻塞其他资源,但损耗较大,目前业界推崇淘汰
img 惨无人道,大家知道有就行了

其实,大家看到这里也就可以了。下文,主要是我对上面代码的一个优化,或者说是Promise实践. 由于懒得开篇幅了,所以就直接接着写。

使用Promise异步加载

前面说了,如果使用像loadScript这种,直接进行回调串行的话,造成的结果是,callback hell;

loadScript("test1.js",loadScript("test2.js",loadScript("test3.js")));

如果了解Promise的童鞋,应该知道,使用Promise就可以完全解决这个问题。 这里,我们使用Promise对上面进行代码进行重构

var loadJs = (function() {
        var script = document.createElement("script");
        if (script.readyState) {
            return function(url) {
                return new Promise(function(res, rej) {
                    script = document.createElement("script");
                    script.src = url;
                    document.body.appendChild(script);
                    script.onreadystatechange = function() {
                        if (script.readyState == "loaded" ||
                            script.readyState == "complete") {
                            script.onreadystatechange = null; //解除引用
                            res();
                        }
                    };
                })
            }
        } else {
            return function(url) {
                return new Promise(function(res, rej) {
                    script = document.createElement("script");
                    script.src = url;
                    document.body.appendChild(script);
                    script.onload = function() {
                        res();
                    };
                })
            }
        }
    })();

接着,我们来调用代码看看:

 loadJs("http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js")
    .then(function(){
        return loadJs("./js/loader01.js");
    }).then(function(){
        console.log("finish loading");
    })

结果是:

那如果我们想并行加载的话,怎么办呢? 很简单使用Promise提供的all函数就可以了.
show u the code:

Promise.all([loadJs("http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js"),loadJs("./js/loader01.js")])

结果为:

OK~ 平时,我们加载模块的时候,就可以使用Promise来进行练习,这样可以减少很多不必要的逻辑代码。简直,赞~(≧▽≦)/~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/78680.html

相关文章

  • 【前端构建】RequireJS及其优化工具

    摘要:介绍一款模块加载工具的入门,并且重点介绍其优化工具。发布目录项目源代码工具目录,例如构建工具等。另外,前端代码发布前都会进行压缩,使文件足够小。原来是因为里了,所以优化工具把也合并进来了。而优化工具要用好,要多尝试他们的配置选项。 前端变化太快,如今RequireJS已经无法吸引眼球了。介绍一款模块加载工具:RequireJS的入门,并且重点介绍其优化工具。 一、RequireJS简介...

    Loong_T 评论0 收藏0
  • 不简单的前端性能优化

    摘要:本文主要介绍关键渲染路径与网络两个方面的性能优化并提供,篇幅较长建议电脑观看。百度统计代码注意,的脚本不会被阻塞,完成后立即执行,但是有可能会阻塞关键渲染路径。 本文主要介绍关键渲染路径与网络两个方面的性能优化并提供demo,篇幅较长建议电脑观看。 前端优化的方面太多,本文介绍的仅仅是其中的一部分,力求涵盖关键渲染路径的方方面面,及一些不常被提到的网络优化部分。 测试环境如无特殊说明均...

    RobinQu 评论0 收藏0
  • 不简单的前端性能优化

    摘要:本文主要介绍关键渲染路径与网络两个方面的性能优化并提供,篇幅较长建议电脑观看。百度统计代码注意,的脚本不会被阻塞,完成后立即执行,但是有可能会阻塞关键渲染路径。 本文主要介绍关键渲染路径与网络两个方面的性能优化并提供demo,篇幅较长建议电脑观看。 前端优化的方面太多,本文介绍的仅仅是其中的一部分,力求涵盖关键渲染路径的方方面面,及一些不常被提到的网络优化部分。 测试环境如无特殊说明均...

    developerworks 评论0 收藏0
  • 不简单的前端性能优化

    摘要:本文主要介绍关键渲染路径与网络两个方面的性能优化并提供,篇幅较长建议电脑观看。百度统计代码注意,的脚本不会被阻塞,完成后立即执行,但是有可能会阻塞关键渲染路径。 本文主要介绍关键渲染路径与网络两个方面的性能优化并提供demo,篇幅较长建议电脑观看。 前端优化的方面太多,本文介绍的仅仅是其中的一部分,力求涵盖关键渲染路径的方方面面,及一些不常被提到的网络优化部分。 测试环境如无特殊说明均...

    hedge_hog 评论0 收藏0
  • 浅谈webpack4.0 性能优化

    摘要:中在性能优化所做的努力,也大抵围绕着这两个大方向展开。因此,将依赖模块从业务代码中分离是性能优化重要的一环。大型库是否可以通过定制功能的方式减少体积。这又违背了性能优化的基础。接下来可以抓住一些细节做更细的优化。中,为默认启动这一优化。 前言:在现实项目中,我们可能很少需要从头开始去配置一个webpack 项目,特别是webpack4.0发布以后,零配置启动一个项目成为一种标配。正因为...

    leanxi 评论0 收藏0

发表评论

0条评论

zhaochunqi

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<