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

幾乎每一個前端程序員都知道應該把script標籤放在頁面底部。關於這個經典的論述可以追溯到Nicholas的 High Performance Javasript 這本書的第一章Loading and Execution中,他之所以建議這麼做是因爲:
  1. 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

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

  1. <head>

  2.     <link rel="stylesheet" type="text/css" href="">

  3.     <script type="text/javascript"></script>

  4. </head>

  5. <body>

  6.     <img src="">

  7.     <img src="">

  8.     <img src="">

  9.     <img src="">

  10.     <img src="">

  11.     <img src="">

  12.     <img src="">

  13.     <img src="">

  14.     <script type="text/javascript"></script>

  15.     <script type="text/javascript"></script>

  16.     <script type="text/javascript"></script>

  17. </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等。並且動態插入頁面的資源無效。

  但細節方面卻值得注意:

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

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

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


  Preloader在響應式設計中的問題

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

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

  1. <html>

  2. <head>

  3.     <title></title>

  4.     <script type="text/javascript" src="./responsive-images.js"></script>

  5. </head>

  6. <body>

  7.     <img src="./running.jpg?medium=_imgs/running.medium.jpg&large=_imgs/running.large.jpg">

  8. </body>

  9. </html>

複製代碼

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

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

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

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

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

  1. <picture>

  2.     <source src="med.jpg" media="(min-width: 40em)" />

  3.     <source src="sm.jpg"/>

  4.     <img src="fallback.jpg" alt="" />

  5. </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秒:

  1. // dep_A.js定義如下,dep_B、dep_C定義同理


  2. define(function(require, exports, module) {

  3.     (function(second) {

  4.         var start = +new Date();

  5.         while (start + second * 1000 > +new Date()) {}

  6.     })(window.EXE_TIME);

  7.     // window.EXE_TIME = 1;此處會連續執行1s


  8.     exports.foo = function() {

  9.         console.log("A");

  10.     }

  11. })

複製代碼

爲了增強對比,設置了三組進行對照試驗,分別是:
  1. //require.js:

  2. require(["dep_A", "dep_B", "dep_C"], function(A, B, C) {

  3. });

  4. //sea.js:

  5. define(function(require, exports, module) {

  6.     var mod_A = require("dep_A");

  7.     var mod_B = require("dep_B");

  8.     var mod_C = require("dep_C");

  9. });

  10. //sea.js(定義依賴但並不require):

  11. define(["dep_A", "dep_B", "dep_C"], function(require, exports, module){

  12. }

複製代碼

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

  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中說道:

  1. 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.

  2. 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元素:

  1. window.onload = function () {

  2.     var i = 0,

  3.         max = 0,

  4.         o = null,

  5.         preload = [

  6.             // list of stuff to preload    

  7.         ],

  8.         isIE = navigator.appName.indexOf('Microsoft') === 0;

  9.     for (i = 0, max = preload.length; i < max; i += 1) {

  10.         if (isIE) {

  11.             new Image().src = preload[i];

  12.             continue;

  13.         }

  14.         o = document.createElement('object');

  15.         o.data = preload[i];

  16.         // IE stuff, otherwise 0x0 is OK

  17.         //o.width = 1;

  18.         //o.height = 1;

  19.         //o.style.visibility = "hidden";

  20.         //o.type = "text/plain"; // IE 

  21.         o.width  = 0;

  22.         o.height = 0;

  23.         // only FF appends to the head

  24.         // all others require body

  25.         document.body.appendChild(o);

  26.     }

  27. };

複製代碼

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

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

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


  type=prefetch

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

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

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


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

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

  • :

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

      但現實並非那麼美好,首先你如何能預測用戶打開的頁面呢,這個功能更適合閱讀或者論壇類型的網站,因爲用戶有很大的概率會往下翻頁;要注意提前的渲染頁面的網絡請求和優先級和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非常值得說明問題:

      在第一個測試中,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的正確載入。我們一起來看看這段代碼。代碼詳解就書寫在註釋中了:

    複製代碼

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

      總結

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

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

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

      其他參考文獻

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

    • Twitter利用LS最主要的記錄了與用戶關聯的信息(截圖自我的Twitter賬號,因爲關注者和被關注者的不同數據會有差異):

    • userAdjacencyList表佔40,158 bytes,用於記錄每個字關聯的用戶信息

    • userHash表佔36,883 bytes,用於記錄用戶被關注的人信息

    • Google利用LS記錄了樣式:


  1. !function (globals, document) {

  2.     var storagePrefix = "mbox_";

  3.     globals.LocalJs = {

  4.         require: function (file, callback) {

  5.             /*

  6.                 如果無法使用localstorage,則使用document.write把需要請求的腳本寫在頁面上

  7.                 作爲fallback,使用document.write確保已經加載了所需要的類庫

  8.             */

  9.             if (!localStorage.getItem(storagePrefix + "jq")) {

  10.                 document.write('<script src="' + file + '" type="text/javascript"></script>');

  11.                 var self = this;

  12.             /*

  13.                 並且3s後再請求一次,但這次請求的目的是爲了獲取jquery源碼,寫入localstorage中(見下方的_loadjs函數)

  14.                 這次“一定”走緩存,不會發出多餘的請求

  15.                 爲什麼會延遲3s執行?爲了確保通過document.write請求jQuery已經加載完成。但很明顯3s也並非一個保險的數值

  16.                 同時使用document.write也是出於需要故意阻塞的原因,而無法爲其添加回調,所以延時3s

  17.             */

  18.                 setTimeout(function () {

  19.                     self._loadJs(file, callback)

  20.                 }, 3e3)

  21.             } else {

  22.                 // 如果可以使用localstorage,則執行注入

  23.                 this._reject(localStorage.getItem(storagePrefix + "jq"), callback)

  24.             }

  25.         },

  26.         _loadJs: function (file, callback) {

  27.             if (!file) {

  28.                 return false

  29.             }

  30.             var self = this;

  31.             var xhr = new XMLHttpRequest;

  32.             xhr.open("GET", file);

  33.             xhr.onreadystatechange = function () {

  34.                 if (xhr.readyState === 4) {

  35.                     if (xhr.status === 200) {

  36.                         localStorage.setItem(storagePrefix + "jq", xhr.responseText)

  37.                     } else {}

  38.                 }

  39.             };

  40.             xhr.send()

  41.         },

  42.         _reject: function (data, callback) {

  43.             var el = document.createElement("script");

  44.             el.type = "text/javascript";

  45.             /*

  46.                 關於如何執行LS中的源碼,我們有三種方式

  47.                 1. eval

  48.                 2. new Function

  49.                 3. 在一段script標籤中插入源碼,再將該script標籤插入頁碼中

  50.                 關於這三種方式的執行效率,我們內部初步測試的結果是不同的瀏覽器下效率各不相同

  51.                 參考一些jsperf上的測試,執行效率甚至和具體代碼有關。

  52.             */

  53.             el.appendChild(document.createTextNode(data));

  54.             document.getElementsByTagName("head")[0].appendChild(el);

  55.             callback && callback()

  56.         },

  57.         isSupport: function () {

  58.             return window.localStorage

  59.         }

  60.     }

  61. }(window, document);

  62. !

  63. function () {

  64.     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";

  65.     url = url.replace(/^\/\/mu[0-9]*\.bdstatic\.com/g, "");

  66.     LocalJs.require(url, function () {})

  67. }();


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章