1. 程式人生 > >js加載優化

js加載優化

js

在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裏面的腳本。

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

他的支持度是<=IE9的.
並且,他的執行順序,是嚴格依賴的,即:

<script src="outside1.js" defer></script><script src="outside2.js" defer></script>

當頁面解析完後,他便會開始按照順序執行 outside1 和 outside2文件。
如果你在IE9以下使用defer的話,可能會遇到 它們兩個不是順序執行的,這裏需要一個hack進行處理,即在兩個中間加上一個空的script標簽

<script src="outside1.js" defer></script><script></script> //hack<script src="outside2.js" defer></script>

但是,如果你將defer屬性用在inline的script腳本裏面,在Chrome和FF下是沒有效果的。
即:

<script type="text/javascript" defer = "defer">   //沒有效果
      console.log("defer doesn't make sense");    </script>

async加載

async是H5新定義的一個script 屬性。 他是另外一種js的加載模式。

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

  2. 繼續解析剩下的文件,同時並行加載script的外部資源

  3. 當script加載完成之後,則瀏覽器暫停解析文檔,將權限交給JS引擎,指定加載的腳本。

  4. 執行完後,則恢復瀏覽器解析腳本

可以看出async也可以解決 阻塞加載 這個問題。不過,async執行的時候是異步執行,造成的是,執行文件的順序不一致。即:

<script src="outside1.js" async></script><script src="outside2.js" async></script>

這時,誰先加載完,就先執行誰。所以,一般依賴文件就不應該使用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以求向下兼容

<script src="test.js" async defer></script> //如果兩者都支持,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
        }
    })();

串行執行的測試結果:

commandJs.add('./js/loader01.js').add('./js/loader02.js').exe();//或者commandJs.add('./js/loader01.js','./js/loader02.js').exe();//這兩種寫法都是可以的

最後的結果是:
技術分享圖片
ok~ 可以通過,這樣可以自定義加載很多依賴文件。 但是,造成結果是,時間成本耗費太大。 有時候, 一個主文件的main 有很多依賴js模塊, 那麽我們考慮一下,能否把這些js模塊並行加載進來呢?

  1. 其實,上面的那一串代碼,已經將串行和並行給結合起來了。那並行是怎麽做的呢? 其實就是,同時向頁面中添加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')])

結果為:
技術分享圖片
平時,我們加載模塊的時候,就可以使用Promise來進行練習,這樣可以減少很多不必要的邏輯代碼。


js加載優化