• <noscript id="eom2a"><optgroup id="eom2a"></optgroup></noscript>
    <tt id="eom2a"><small id="eom2a"></small></tt>
    <input id="eom2a"></input>
  • <div id="eom2a"><small id="eom2a"></small></div>
    <td id="eom2a"><small id="eom2a"></small></td>
  • 您的位置:知識庫 ? Web前端

    讓我們再聊聊瀏覽器資源加載優化

    作者: 李光毅  來源: InfoQ  發布時間: 2014-07-09 22:50  閱讀: 13830 次  推薦: 12   原文鏈接   [收藏]  

      幾乎每一個前端程序員都知道應該把script標簽放在頁面底部。關于這個經典的論述可以追溯到Nicholas的 High Performance Javasript 這本書的第一章Loading and Execution中,他之所以建議這么做是因為:

    Put all <script> tags at the bottom of the page, just inside of the closing </body> tag. This ensures that the page can be almost completely rendered before script execution begins.

      簡而言之,如果瀏覽器加載并執行腳本,會引起頁面的渲染被暫停,甚至還會阻塞其他資源(比如圖片)的加載。為了更快的給用戶呈現網頁內容,更好的用戶體驗,應該把腳本放在頁面底部,使之最后加載。

      為什么要在標題中使用“再”這個字?因為在工作中逐漸發現,我們經常談論的一些頁面優化技巧,比如上面所說的總是把腳本放在頁面的底部,壓縮合并樣式或者腳本文件等,時至今日已不再是最佳的解決方案,甚至事與愿違,轉化為性能的毒藥。這篇文章所要聊的,便是展示某些不被人關注的瀏覽器特性或者技巧,來繼續完成資源加載性能優化的任務。

      一. Preloader

      什么是Preloader

      首先讓我們看一看這樣一類資源分布的頁面

    <head>
        <link rel="stylesheet" type="text/css" href="">
        <script type="text/javascript"></script>
    </head>
    <body>
        <img src="">
        <img src="">
        <img src="">
        <img src="">
        <img src="">
        <img src="">
        <img src="">
        <img src="">
        <script type="text/javascript"></script>
        <script type="text/javascript"></script>
        <script type="text/javascript"></script>
    </body>

      這類頁面的特點是,一個外鏈腳本置于頁面頭部,三個外鏈腳本置于頁面的底部,并且是故意跟隨在一系列img之后,在Chrome中頁面加載的網絡請求瀑布圖如下:

      值得注意的是,雖然腳本放置在圖片之后,但加載仍先于圖片。為什么會出現這樣的情況?為什么故意置后資源能夠提前得到加載?

      雖然瀏覽器引擎的實現不同,但原理都十分的近似。不同瀏覽器的制造廠商們(vendor)非常清楚瀏覽器的瓶頸在哪(比如network, javascript evaluate, reflow, repaint)。針對這些問題,瀏覽器也在不斷的進化,所以我們才能看到更快的腳本引擎,調用GPU的渲染等一推陳出新的優化技術和方案。

      同樣在資源加載上,早在IE8開始,一種叫做lookahead pre-parser(在Chrome中稱為preloader)的機制就已經開始在不同瀏覽器中興起。IE8相對于之前IE版本的提升除了將每臺host最高并行下載的資源數從2提升至6,并且能夠允許并行下載腳本文件之外,最后就是這個lookahead pre-parser機制

      但我還是沒有詳述這是一個什么樣的機制,不著急,首先看看與IE7的對比:

      以上面的頁面為例,我們看看IE7下的瀑布圖:

      底部的腳本并沒有提前被加載,并且因為由于單個域名最高并行下載數2的限制,資源總是兩個兩個很整齊的錯開并行下載。

      但在IE8下,很明顯底部腳本又被提前:

      并沒有統一的標準規定這套機制應具備何種功能已經如何實現。但你可以大致這么理解:瀏覽器通常會準備兩個頁面解析器parser,一個(main parser)用于正常的頁面解析,而另一個(preloader)則試圖去文檔中搜尋更多需要加載的資源,但這里的資源通常僅限于外鏈的js、stylesheet、image;不包括audio、video等。并且動態插入頁面的資源無效。

      但細節方面卻值得注意:

    1. 比如關于preloader的觸發時機,并非與解析頁面同時開始,而通常是在加載某個head中的外鏈腳本阻塞了main parser的情況下才啟動;

    2. 也不是所有瀏覽器的preloader會把圖片列為預加載的資源,可能它認為圖片加載過于耗費帶寬而不把它列為預加載資源之列;

    3. preloader也并非最優,在某些瀏覽器中它會阻塞body的解析。因為有的瀏覽器將頁面文檔拆分為head和body兩部分進行解析,在head沒有解析完之前,body不會被解析。一旦在解析head的過程中觸發了preloader,這無疑會導致head的解析時間過長。

      Preloader在響應式設計中的問題

      preloader的誕生本是出于一番好意,但好心也有可能辦壞事。

      filamentgroup有一種著名的響應式設計的圖片解決方案Responsive Design Images

    <html>
    <head>
        <title></title>
        <script type="text/javascript" src="./responsive-images.js"></script>
    </head>
    <body>
        <img src="./running.jpg?medium=_imgs/running.medium.jpg&large=_imgs/running.large.jpg">
    </body>
    </html>

      它的工作原理是,當responsive-images.js加載完成時,它會檢測當前顯示器的尺寸,并且設置一個cookie來標記當前尺寸。同時你需要在服務器端準備一個.htaccess文件,接下來當你請求圖片時,.htaccess中的配置會檢測隨圖片請求異同發送的Cookie是被設置成medium還是large,這樣也就保證根據顯示器的尺寸來加載對于的圖片大小。

      很明顯這個方案成功的前提是,js執行先于發出圖片請求。但在Chrome下打開,你會發現執行順序是這樣:

      responsive-images.js和圖片幾乎是同一時間發出的請求。結果是第一次打開頁面給出的是默認小圖,如果你再次刷新頁面,因為Cookie才設置成功,服務器返回的是大圖。

      嚴格意義上來說在某些瀏覽器中這不一定是preloader引起的問題,但preloader引起的問題類似:插入腳本的順序和位置或許是開發者有意而為之的,但preloader的這種“聰明”卻可能違背開發者的意圖,造成偏差。

      如果你覺得上一個例子還不夠說明問題的話,最后請考慮使用picture(或者@srcset)元素的情況:

    <picture>
        <source src="med.jpg" media="(min-width: 40em)" />
        <source src="sm.jpg"/>
        <img src="fallback.jpg" alt="" />
    </picture>

      在preloader搜尋到該元素并且試圖去下載該資源時,它應該怎么辦?一個正常的paser應該是在解析該元素時根據當時頁面的渲染布局去下載,而當時這類工作不一定已經完成,preloader只是提前找到了該元素。退一步來說,即使不考慮頁面渲染的情況,假設preloader在這種情形下會觸發一種默認加載策略,那應該是"mobile first"還是"desktop first"?默認應該加載高清還是低清照片?

      二. JS Loader

      理想是豐滿的,現實是骨感的。出于種種的原因,我們幾乎從不直接在頁面上插入js腳本,而是使用第三方的加載器,比如seajs或者requirejs。關于使用加載器和模塊化開發的優勢在這里不再贅述。但我想回到原點,討論應該如何利用加載器,就從seajs與requirejs的不同聊起。

      在開始之前我已經假設你對requirejs與seajs語法已經基本熟悉了,如果還沒有,請移步這里:

      BTW: 如果你還是習慣在部署上線前把所有js文件合并打包成一個文件,那么seajs和requirejs其實對你來說并無區別。

      seajs與requirejs在模塊的加載方面是沒有差異的,無論是requirejs在定義模塊時定義的依賴模塊,還是seajs在factory函數中require的依賴模塊,在會在加載當前模塊時被載入,異步,并且順序不可控。差異在于factory函數執行的時機。

      執行差異

      為了增強對比,我們在定義依賴模塊的時候,故意讓它們的factory函數要執行相當長的時間,比如1秒:

    // dep_A.js定義如下,dep_B、dep_C定義同理
    
    define(function(require, exports, module) {
    
        (function(second) {
            var start = +new Date();
            while (start + second * 1000 > +new Date()) {}
        })(window.EXE_TIME);
    
        // window.EXE_TIME = 1;此處會連續執行1s
    
        exports.foo = function() {
            console.log("A");
        }
    })

      為了增強對比,設置了三組進行對照試驗,分別是:

    //require.js:
    require(["dep_A", "dep_B", "dep_C"], function(A, B, C) {
    
    });
    
    //sea.js:
    define(function(require, exports, module) {
    
        var mod_A = require("dep_A");
        var mod_B = require("dep_B");
        var mod_C = require("dep_C");
    });
    
    //sea.js(定義依賴但并不require):
    define(["dep_A", "dep_B", "dep_C"], function(require, exports, module){
    
    }

      接下來我們看看代碼執行的瀑布圖:

      1.require.js:在加載完依賴模塊之后立即執行了該模塊的factory函數

      2.sea.js: 下面兩張圖應該放在一起比較。兩處代碼都同時加載了依賴模塊,但因為沒有require的關系,第三張圖中沒有像第二張圖那樣執行耗時的factory函數。可見seajs執行的原則正如CMD標準中所述Execution must be lazy。

      我想進一步表達的是,無論requirejs和seajs,通常來說大部分的邏輯代碼都會放在模塊的factory函數中,所以factory函數執行的代價是非常大的。但上圖也同樣告訴我們模塊的define,甚至模塊文件的Evaluate代價非常小,與factory函數無關。所以我們是不是應該盡可能的避免執行factory函數,或者等到我們需要的指定功能的時候才執行對應的factory函數?比如:

    document.body.onclick = function () {
        require(some_kind_of_module);
    }

      這是非常實際的問題,比如愛奇藝一個視頻播放的頁面,我們有沒有必要在第一屏加載頁面的時候就加載登陸注冊,或者評論,或者分享功能呢?因為有非常大的可能用戶只是來這里看這個視頻,直至看完視頻它都不會用到登陸注冊功能,也不會去分享這個視頻等。加載這些功能不僅僅對瀏覽器是一個負擔,還有可能調用后臺的接口,這樣的性能消耗是非常可觀的。

      我們可以把這樣稱之為"懶執行"。雖然seajs并非有意實現如上所說的“懶執行”(它只是在盡可能遵循CommonJS標準靠近)。但“懶執行”確實能夠有助于提升一部分性能。

      但也有人會對此產生顧慮。

      記得玉伯轉過的一個帖子:SeaJS與RequireJS最大的區別。我們看看其中反對這么做的人的觀點

    我個人感覺requirejs更科學,所有依賴的模塊要先執行好。如果A模塊依賴B。當執行A中的某個操doSomething()后,再去依賴執行B模塊require('B');如果B模塊出錯了,doSomething的操作如何回滾? 很多語言中的import, include, useing都是先將導入的類或者模塊執行好。如果被導入的模塊都有問題,有錯誤,執行當前模塊有何意義?

    而依賴dependencies是工廠的原材料,在工廠進行生產的時候,是先把原材料一次性都在它自己的工廠里加工好,還是把原材料的工廠搬到當前的factory來什么時候需要,什么時候加工,哪個整體時間效率更高?

      首先回答第一個問題。

      第一個問題的題設并不完全正確,“依賴”和“執行”的概念比較模糊。編程語言執行通常分為兩個階段,編譯(compilation)和運行(runtime)。對于靜態語言(比如C/C++)來說,在編譯時如果出現錯誤,那可能之前的編譯都視為無效,的確會出現描述中需要回滾或者重新編譯的問題。但對于動態語言或者腳本語言,大部分執行都處在運行時階段或者解釋器中:假設我使用Nodejs或者Python寫了一段服務器運行腳本,在持續運行了一段時間之后因為某項需求要加載某個(依賴)模塊,同時也因為這個模塊導致服務端掛了——我認為這時并不存在回滾的問題。在加載依賴模塊之前當前的模塊的大部分功能已經成功運行了。

      再回答第二個問題。

      對于“工廠”和“原材料”的比喻不夠恰當。難道依賴模塊沒有加載完畢當前模塊就無法工作嗎?requirejs的確是這樣的,從上面的截圖可以看出,依賴模塊總是先于當前模塊加載和執行完畢。但我們考慮一下基于CommonJS標準的Nodejs的語法,使用require函數加載依賴模塊可以在頁面的任何位置,可以只是在需要的時候。也就是說當前模塊不必在依賴模塊加載完畢后才執行。

      你可能會問,為什么要拿AMD標準與CommonJS標準比較,而不是CMD標準?

      玉伯在CommonJS 是什么這篇文章中已經告訴了我們CMD某種程度上遵循的就是CommonJS標準:

    從上面可以看出,Sea.js 的初衷是為了讓 CommonJS Modules/1.1 的模塊能運行在瀏覽器端,但由于瀏覽器和服務器的實質差異,實際上這個夢無法完全達成,也沒有必要去達成。

    更好的一種方式是,Sea.js 專注于 Web 瀏覽器端,CommonJS 則專注于服務器端,但兩者有共通的部分。對于需要在兩端都可以跑的模塊,可以 有便捷的方案來快速遷移。

      其實AMD標準的推出同時也是遵循CommonJS,在requirejs官方文檔的COMMONJS NOTES中說道:

    CommonJS defines a module format. Unfortunately, it was defined without giving browsers equal footing to other JavaScript environments. Because of that, there are CommonJS spec proposals for Transport formats and an asynchronous require.

    RequireJS tries to keep with the spirit of CommonJS, with using string names to refer to dependencies, and to avoid modules defining global objects, but still allow coding a module format that works well natively in the browser.

      CommonJS當然是一個理想的標準,但至少現階段對瀏覽器來說還不夠友好,所以才會出現AMD與CMD,其實他們都是在做同一件事,就是致力于前端代碼更友好的模塊化。所以個人認為依賴模塊的加載和執行在不同標準下實現不同,可以理解為在用不同的方式在完成同一個目標, 并不是一件太值得過于糾結的事。

      懶加載

      其實我們可以走的更遠,對于非必須模塊不僅僅可以延遲它的執行,甚至可以延遲它的加載。

      但問題是我們如何決定一個模塊是必須還是非必須呢,最恰當莫過取決于用戶使用這個模塊的概率有多少。Faceboook早在09年的時候就已經注意到這個問題:Frontend Performance Engineering in Facebook : Velocity 2009,只不過他們是以樣式碎片來引出這個問題。

      假設我們需要在頁面上加入A、B、C三個功能,意味著我們需要引入A、B、C對應的html片段和樣式碎片(暫不考慮js),并且最終把三個功能樣式碎片在上線前壓縮到同一個文件中。但可能過了相當長時間,我們移除了A功能,但這個時候大概不會有人記得也把關于A功能的樣式從上線樣式中移除。久而久之冗余的代碼會變得越來越多。Facebook引入了一套靜態資源管理方案(Static Resource Management)來解決這個問題:

      具體來說是將樣式的“聲明”(Declaration)和請求(Delivery)請求,并且是否請求一個樣式由是否擁有該功能的 html片段決定。

      當然同時也考慮也會適當的合并樣式片段,但這完全是基于使用算法對用戶使用模塊情況進行分析,挑選出使用頻率比較高的模塊進行拼合。

      這一套系統不僅僅是對樣式碎片,對js,對圖片sprites的拼合同樣有效。

      你會不會覺得我上面說的懶加載還是離自己太遠了? 但然不是,你去看看現在的人人網個人主頁看看

      如果你在點擊圖中標注的“與我相關”、“相冊”、“分享”按鈕并觀察Chrome的Timeline工具,那么都是在點擊之后才加載對應的模塊

      三. Delay Execution

      利用瀏覽器緩存

      腳本最致命的不是加載,而是執行。因為何時加載畢竟是可控的,甚至可以是異步的,比如通過調整外鏈的位置,動態的創建腳本。但一旦腳本加載完成,它就會被立即執行(Evaluate Script),頁面的渲染也就隨之停止,甚至導致在低端瀏覽器上假死。

      更加充分的理由是,大部分的頁面不是Single Page Application,不需要依靠腳本來初始化頁面。服務器返回的頁面是立即可用的,可以想象我們初始化腳本的時間都花在用戶事件的綁定,頁面信息的豐滿(用戶信息,個性推薦)。Steve Souders發現在Alexa上排名前十的美國網站上的js代碼,只有29%在window.onload事件之前被調用,其他的71%的代碼與頁面的渲染無關。

      Steve Souders的ControlJS是我認為一直被忽視的一個加載器,它與Labjs一樣能夠控制的腳本的異步加載,甚至(包括行內腳本,但不完美)延遲執行。它延遲執行腳本的思路非常簡單:既然只要在頁面上插入腳本就會導致腳本的執行,那么在需要執行的時候才把腳本插入進頁面。但這樣一來腳本的加載也被延遲了?不,我們會通過其他元素來提前加載腳本,比如img或者是object標簽,或者是非法的mine type的script標簽。這樣當真正的腳本被插入頁面時,只會從緩存中讀取。而不會發出新的請求。

      Stoyan Stefanov在它的文章Preload CSS/JavaScript without execution中詳細描述了這個技巧,   如果判斷瀏覽器是IE就是用image標簽,如果是其他瀏覽器,則使用object元素:

    window.onload = function () {
    
        var i = 0,
            max = 0,
            o = null,
    
            preload = [
                // list of stuff to preload    
            ],
    
            isIE = navigator.appName.indexOf('Microsoft') === 0;
    
        for (i = 0, max = preload.length; i < max; i += 1) {
    
            if (isIE) {
                new Image().src = preload[i];
                continue;
            }
            o = document.createElement('object');
            o.data = preload[i];
    
            // IE stuff, otherwise 0x0 is OK
            //o.width = 1;
            //o.height = 1;
            //o.style.visibility = "hidden";
            //o.type = "text/plain"; // IE 
            o.width  = 0;
            o.height = 0;
    
    
            // only FF appends to the head
            // all others require body
            document.body.appendChild(o);
        }
    
    };

      同時它還列舉了其他的一些嘗試,但并非對所有的瀏覽器都有效,比如:

    • 使用<link>元素加載script,這么做在Chrome中的風險是,在當前頁有效,但是在以后打開需要使用該腳本的頁面會無視該文件為緩存

    • 改變script標簽外鏈的type值,比如改為text/cache來阻止腳本的執行。這么做會導致在某些瀏覽器(比如FF3.6)中壓根連請求都不會發出

      type=prefetch

      延遲執行并非僅僅作為當前頁面的優化方案,還可以為用戶可能打開的頁面提前緩存資源,如果你對這兩種類型的link元素熟悉的話:

    • <link rel="subresource" href="jquery.js">: subresource類型用于加載當前頁面將使用(但還未使用)的資源(預先載入緩存中),擁有較高優先級

    • <link rel="prefetch" >: prefetch類型用于加載用戶將會打開頁面中使用到的資源,但優先級較低,也就意味著瀏覽器不做保證它能夠加載到你指定的資源。

      那么上一節延遲執行的方案就可以作為subresource與prefeth的回滾方案。同時還有其他的類型:

    • <link rel="dns-prefetch" href="http://host_name_to_prefetch.com">: dns-prefetch類型用于提前dns解析和緩存域名主機信息,以確保將來再請求同域名的資源時能夠節省dns查找時間,比如我們可以看到淘寶首頁就使用了這個類型的標簽:

    • <link rel="prerender" >: prerender類型就比較霸道了,它告訴瀏覽器打開一個新的標簽頁(但不可見)來渲染指定頁面,比如這個頁面:

      這也就意味著如果用戶真的訪問到該頁面時,就會有“秒開”的用戶體驗。

      但現實并非那么美好,首先你如何能預測用戶打開的頁面呢,這個功能更適合閱讀或者論壇類型的網站,因為用戶有很大的概率會往下翻頁;要注意提前的渲染頁面的網絡請求和優先級和GPU使用權限優先級都比其他頁面的要低,瀏覽器對提前渲染頁面類型也有一定的要求,具體可以參考這里

      利用LocalStorage

      在聊如何用它來解決我們遇到的問題之前,個人覺得首先應該聊聊它的優勢和劣勢。

      Chris Heilmann在文章There is no simple solution for local storage中指出了一些常見的LS劣勢,比如同步時可能會阻塞頁面的渲染、I/O操作會引起不確定的延時、持久化機制會導致冗余的數據等。雖然Chirs在文章中用到了比如"terrible performance", "slow"等字眼,但卻沒有真正的指出究竟是具體的哪一項操作導致了性能的低下。

      Nicholas C. Zakas于是寫了一篇針對該文的文章In defense of localStorage,從文章的名字就可以看出,Nicholas想要捍衛LS,畢竟它不是在上一文章中被描述的那樣一無是處,不應該被抵制。

      比較性能這種事情,應該看怎么比,和誰比。

      就“讀”數據而言,如果你把“從LS中讀一個值”和“從Object對象中讀一個屬性”相比,是不公平的,前者是從硬盤里讀,后者是從內存里讀,就好比讓汽車與飛機賽跑一樣,有一個benchmark各位可以參考一下:localStorage vs. Objects:

      跑分的標準是OPS(operation per second),值當然是越高越好。你可能會注意到,在某個瀏覽器的對比列中,沒有顯示關于LS的紅色列——這不是因為統計出錯,而是因為LS的操作性能太差,跑分太低(相對從Object中讀取屬性而言),所以無法顯示在同一張表格內,如果你真的想看的話,可以給你看一張放大的版本:

      這樣以來你大概就知道兩者在什么級別上了。

      在瀏覽器中與LS最相近的機制莫過于Cookie了:Cookie同樣以key-value的形式進行存儲,同樣需要進行I/O操作,同樣需要對不同的tab標簽進行同步。同樣有benchmark可以供我們進行參考:localStorage vs. Cookies

      從Brwoserscope中提供的結果可以看出,就Reading from cookie, Reading from localStorage getItem, Writing to cookie,Writing to localStorage property四項操作而言,在不同瀏覽器不同平臺,讀和寫的效率都不太相同,有的趨于一致,有的大相徑庭。

      甚至就LS自己而言,不同的存儲方式和不同的讀取方式也會產生效率方面的問題。有兩個benchmark非常值得說明問題:

    1. localStorage-string-size

    2. localStorage String Size Retrieval

      在第一個測試中,Nicholas在LS中用四個key分別存儲了100個字符,500個字符,1000個字符和2000個字符。測試分別讀取不同長度字符的速度。結果是:讀取速度與讀取字符的長度無關

      第二個測試用于測試讀取1000個字符的速度,不同的是對照組是一次性讀取1000個字符;而實驗組是從10個key中(每個key存儲100個字符)分10次讀取。結論: 是分10此讀取的速度會比一次性讀取慢90%左右

      LS也并非沒有痛點。大部分的LS都是基于同一個域名共享存儲數據,所以當你在多個標簽打開同一個域名下的站點時,必須面臨一個同步的問題,當A標簽想寫入LS與B標簽想從LS中讀同時發生時,哪一個操作應該首先發生?為了保證數據的一致性,在讀或者在寫時 務必會把LS鎖住(甚至在操作系統安裝的殺毒軟件在掃描到該文件時,會暫時鎖住該文件)。因為單線程的關系,在等待LS I/O操作的同時,UI線程和Javascript也無法被執行。

      但實際情況遠比我們想象的復雜的多。為了提高讀寫的速度,某些瀏覽器(比如火狐)會在加載頁面時就把該域名下LS數據加載入內存中,這么做的副作用是延遲了頁面的加載速度。但如果不這么做而是在臨時讀寫LS時再加載,同樣有死鎖瀏覽器的風險。并且把數據載入內存中也面臨著將內存同步至硬盤的問題。

      上面說到的這些問題大部分歸咎于內部的實現,需要依賴瀏覽器開發者來改進。并且并非僅僅存在于LS中,相信在IndexedDB、webSQL甚至Cookie中也有類似的問題在發生。

      實戰開始

      考慮到移動端網絡環境的不穩定,為了避免網絡延遲(network latency),大部分網站的移動端站點會將體積龐大的類庫存儲于本地瀏覽器的LS中。但百度音樂將這個技術也應用到了PC端,他們將所依賴的jQuery類庫存入LS中。用一段很簡單的代碼來保證對jQuery的正確載入。我們一起來看看這段代碼。代碼詳解就書寫在注釋中了:

    !function (globals, document) {
        var storagePrefix = "mbox_";
        globals.LocalJs = {
            require: function (file, callback) {
                /*
                    如果無法使用localstorage,則使用document.write把需要請求的腳本寫在頁面上
                    作為fallback,使用document.write確保已經加載了所需要的類庫
                */
    
                if (!localStorage.getItem(storagePrefix + "jq")) {
                    document.write('<script src="' + file + '" type="text/javascript"></script>');
                    var self = this;
    
                /*
                    并且3s后再請求一次,但這次請求的目的是為了獲取jquery源碼,寫入localstorage中(見下方的_loadjs函數)
                    這次“一定”走緩存,不會發出多余的請求
                    為什么會延遲3s執行?為了確保通過document.write請求jQuery已經加載完成。但很明顯3s也并非一個保險的數值
                    同時使用document.write也是出于需要故意阻塞的原因,而無法為其添加回調,所以延時3s
                */
                    setTimeout(function () {
                        self._loadJs(file, callback)
                    }, 3e3)
                } else {
                    // 如果可以使用localstorage,則執行注入
                    this._reject(localStorage.getItem(storagePrefix + "jq"), callback)
                }
            },
            _loadJs: function (file, callback) {
                if (!file) {
                    return false
                }
                var self = this;
                var xhr = new XMLHttpRequest;
                xhr.open("GET", file);
                xhr.onreadystatechange = function () {
                    if (xhr.readyState === 4) {
                        if (xhr.status === 200) {
                            localStorage.setItem(storagePrefix + "jq", xhr.responseText)
                        } else {}
                    }
                };
                xhr.send()
            },
            _reject: function (data, callback) {
                var el = document.createElement("script");
                el.type = "text/javascript";
                /*
                    關于如何執行LS中的源碼,我們有三種方式
                    1. eval
                    2. new Function
                    3. 在一段script標簽中插入源碼,再將該script標簽插入頁碼中
    
                    關于這三種方式的執行效率,我們內部初步測試的結果是不同的瀏覽器下效率各不相同
                    參考一些jsperf上的測試,執行效率甚至和具體代碼有關。
                */
                el.appendChild(document.createTextNode(data));
                document.getElementsByTagName("head")[0].appendChild(el);
                callback && callback()
            },
            isSupport: function () {
                return window.localStorage
            }
        }
    }(window, document);
    !
    function () {
        var url = _GET_HASHMAP ? _GET_HASHMAP("/player/static/js/naga/common/jquery-1.7.2.js") : "/player/static/js/naga/common/jquery-1.7.2.js";
        url = url.replace(/^\/\/mu[0-9]*\.bdstatic\.com/g, "");
        LocalJs.require(url, function () {})
    }(); 

      因為桌面端的瀏覽器兼容性問題比移動端會嚴峻的多,所以大多數對LS利用屬于“做加法”,或者“輕量級”的應用。最后一瞥不同站點在PC平臺的對LS的使用情況:

      • 比如百度和github用LS記錄用戶的搜素行為,為了提供更好的搜索建議

      • Twitter利用LS最主要的記錄了與用戶關聯的信息(截圖自我的Twitter賬號,因為關注者和被關注者的不同數據會有差異):
      • userAdjacencyList表占40,158 bytes,用于記錄每個字關聯的用戶信息
      • userHash表占36,883 bytes,用于記錄用戶被關注的人信息

      • Google利用LS記錄了樣式:

      • 天貓用LS記錄了導航欄的HTML碎片代碼:

      總結

      No silver bullet.沒有任何一項技術或者方案是萬能的,雖然開源社區和瀏覽器廠商在提供給我們越來越豐富的資源,但并不意味著今后遇見的問題就會越來越少。相反,或許正因為多樣性,和發展中技術的不完善,事情會變得更復雜,我們在選擇時要權衡更多。我無意去推崇某一項解決方案,我想盡可能多的把這些方案與這些方案的厲害呈現給大家,畢竟不同人考慮問題的方面不同,業務需求不同。

      還有一個問題是,本文描述的大部分技術都是針對現代瀏覽器而言,那么如何應對低端瀏覽器呢?

      從百度統計這張17個月的瀏覽器市場份額圖中可以看出(當然可能因為不同站點的用戶特征不同會導致使用的瀏覽器分布與上圖有出入),我們最關心的IE6的市場份額一直是呈現的是下滑的趨勢,目前已經降至幾乎與IE9持平;而IE9在今年的市場份額也一直穩步上升;IE7已經被遙遙甩在身后。領頭的IE8與Chrome明顯讓我們感受到有足夠的信心去嘗試新的技術。還等什么,行動起來吧!

      其他參考文獻

    12
    0

    Web前端熱門文章

      Web前端最新文章

        最新新聞

          熱門新聞

            黄色网_免费在线黄色电影_黄色成人快播电影_伦理电影_黄色片