標籤:
前言
在之前的文章 如何最佳化網站效能,提高頁面載入速度 中,我們簡單介紹了網站效能最佳化的重要性以及幾種網站效能最佳化的方法(沒有看過的可以狂戳 連結 移步過去看一下),那麼今天我們深入討論如何進一步最佳化網站效能。
一、拆分初始化負載
拆分初始化負載——聽名字覺得高大上,其實不然,土一點將講就是將頁面載入時需要的一堆JavaScript檔案,分成兩部分:渲染頁面所必需的(頁面出來,沒他不行)和剩下的。頁面初始化時,只載入必須的,其餘的等會載入。
其實在現實生產環境中,對於大部分網站:頁面載入完畢(window.onload觸發)時,已經執行的JavaScript函數只佔到全部載入量的少部分,譬如10%到20%或者更少。
注意:這裡所說的頁面載入完畢是指window.onload觸發。window.onload什麼時候出發?當頁面中的內容(包括圖片、樣式、指令碼)全部載入到瀏覽器時,才會觸發window.onload,請與jQuery中$(document).ready作區分。
上面我們可以看到大部分JavaScript函數下載之後並未執行,這就造成了浪費。因此,如果我們能夠使用某種方式來延遲這部分未使用的代碼的載入,那想必可以極大的縮減頁面初始化時候的下載量。
拆分檔案
我們可以將原來的代碼檔案拆分成兩部分:渲染頁面所必需的(頁面出來,沒他不行)和剩下的;頁面載入時只載入必須的,剩餘的JavaScript代碼在頁面載入完成之後採用無阻塞下載技術立即下載。
需要注意的問題:
1. 我們可以通過某些工具(譬如:Firebug)來獲得頁面載入時執行的函數,從而將這些代碼拆分成一個單獨的檔案。那麼問題來了,有些代碼在頁面載入的時候不會執行,但是確實必須的,譬如條件判斷代碼或者錯誤處理的代碼。另外JavaScript的範圍問題是相對比較奇葩的,這些都給拆分造成了很大的困難
2. 關於未定義標識符的錯誤,譬如已載入的JavaScript代碼在執行時,引用了一個被我們拆分消極式載入的JavaScript代碼中的變數,就會造成錯誤。舉個栗子:
頁面載入完成時使用者點擊了某個按鈕(此時原JavaScript檔案被拆分,只下載了頁面載入所必需的的代碼),而監聽此按鈕的代碼還沒有被下載(因為這不是頁面載入所必需的,所以在拆分時被降級了),所以點擊就沒有響應或者直接報錯(找不到事件處理函數)。
解決方案:
1. 在低優先順序的代碼被載入完成時,按鈕處於不可用狀態(可附帶提示資訊);
2. 使用樁函數,樁函數與原函數名字相同,但是函數體為空白,這樣就可以防止報錯了。當剩餘的代碼載入完成時,樁函數就被原來的同名函數覆蓋掉。我們可以做的再狠一點:記錄使用者的行為(點擊、下拉),當剩餘的代碼載入完成時,再根據記錄調用相應的函數。
二、無阻塞載入指令碼
大多數瀏覽器可以並行下載頁面所需要的組件,然而對於指令檔卻並非如此。指令檔在下載時,在其下載完成、解析執行完畢之前,並不會下載任何其他的內容。這麼做是有道理的,因為瀏覽器並不知道指令碼是否會操作頁面的內容;其次,後面載入的指令碼可能會依賴前面的指令碼 ,如果並行下載,後面的指令碼可能會先下載完並執行,產生錯誤。所以,之前我們講到了指令碼應該儘可能放在底部接近</body>的位置,就是為了盡量減少整個頁面的影響。
接下來我們討論幾種技術可以使頁面不會被指令碼的下載阻塞:
1、Script Defer
<script type="text/javascript" src="file1.js" defer></script>
支援瀏覽器: IE4+ 、Firefox 3.5+以及其它新版本的瀏覽器
defer表示該指令碼不打算修改DOM,可以稍後執行。
2、動態指令碼元素
var script = document.createElement ("script");script.type = "text/javascript";script.src = "a.js"; document.body.appendChild(script);
用動態建立script標籤的方法不會阻塞其它的頁面處理過程,在IE下還可以並行下載指令碼。
3、XHR(XMLHttpRequest)Eval
該方法通過XMLHttpRequest以非阻塞的方式從服務端載入指令碼,載入完成之後通過eval解析執行。
1 var xhr = getXHRObj(); 2 3 xhr.onreadystatechange = function() { 4 if(xhr.readyState == 4 && xhr.status == 200) { 5 eval(xhr.responseText); 6 } 7 }; 8 9 xhr.open(‘GET‘,‘text.js‘,true);10 xhr.send(‘‘);11 12 function getXHRObj() {13 // ......14 return xhrObj;15 }
該方式不會阻塞頁面中其它組件的下載。
缺點:(1)指令碼的域必須和首頁面在相同的域中;(2)eval的安全性問題
4、XHR Injection
XMLHttpRequest Injection(XHR指令碼注入)和XHR Eval類似,都是通過 XMLHttpRequest 來擷取JavaScript的。 在獲得檔案之後 ,將會建立一個script標籤將得到的代碼注入頁面。
1 var xhr = new XMLHttpRequest(); 2 xhr.open("GET", "test.js", true); 3 xhr.send(‘‘); 4 xhr.onreadystatechange = function(){ 5 if (xhr.readyState == 4){ 6 if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ 7 var script = document.createElement("script"); 8 script.type = "text/javascript"; 9 script.text = xhr.responseText;10 document.body.appendChild(script);11 } 12 }13 };
XMLHttpRequest擷取的內容必須和首頁處於相同的域。
5、Script元素的src屬性
1 var script = document.createElement(‘script‘);2 script.src = ‘http://a.com/a.js‘3 document.body.appendChild(script);
這種方式不會阻塞其它組件,而且允許跨域擷取指令碼。
6、IFrame嵌入Script
頁面中的iframe和其它元素是並行下載的,因此可以利用這點將需要載入的指令碼嵌入iframe中。
<iframe src="1.html" frameborder="0" width=0 height="0"></iframe>
注意:這裡是1.html而不是1.js,iframe以為這是html檔案,而我們則把要載入的指令碼嵌入其中。
這種方式要求iframe的請求url和首頁面同域。
三、整合非同步指令碼
上面我們介紹了如何非同步載入指令碼,提高頁面的載入速度。但是非同步載入指令碼也是存在問題的,譬如行內指令碼依賴外部指令碼裡面定義的標識,這樣當內聯的指令碼執行的時候外部指令碼還沒有載入完成,那麼就會發生錯誤。
那麼接下來我們就討論一下如何?在非同步載入指令碼的時候又能保證指令碼的能夠按照正確的順序執行。
單個外部指令碼與內聯指令碼
譬如:內聯指令碼使用了外部指令碼定義的標識符,外部指令碼採用非同步載入提高載入速度
$(".button").click(function() { alert("hello");});
<script src="jquery.js"></script>
1、Script Onload
通過Script的onload方法監聽指令碼是否載入完成,將依賴外部檔案的內聯代碼寫在init函數中,在onload事件函數中調用init函數。
script.onload的支援情況:
IE6、IE7、IE8不支援onload,可以用onreadystatechange來代替。
IE9、IE10先觸發onload事件,再觸發onreadystatechange事件
IE11(Edge)只觸發onload事件
其他瀏覽器支援均支援onload,在opera中onload和onreadystatechange均有效。
1 function init() { 2 // inline code...... 3 } 4 var script = document.createElement("script"); 5 script.type = "text/javascript"; 6 script.src = "a.js"; 7 script.onloadDone = false; 8 9 script.onreadystatechange = function(){ 10 if((script.readyState == ‘loaded‘ || script.readyState == ‘complete‘) && !script.onloadDone){ 11 // alert("onreadystatechange"); 12 init();13 }14 }15 16 script.onload = function(){ 17 // alert("onload");18 init();19 script.onloadDone = true;20 } 21 22 document.getElementsByTagName(‘head‘)[0].appendChild(script);
這裡onloadDone用來防止在IE9、IE10已結opera中初始化函數執行兩次。
Script Onload是整合內聯指令碼和外部非同步載入指令碼的首選。
推薦指數:5顆星
2、寫入程式碼回調
將依賴外部檔案的內聯代碼寫在init函數中,修改非同步載入的檔案,在檔案中添加對init函數的調用。
缺點:要修改外部檔案,而我們一般不會修改第三方的外掛程式;缺乏靈活性,改變回調介面時,需要修改外部的指令碼。
推薦指數:2顆星
3、定時器
將依賴外部檔案的內聯代碼寫在init函數中,採用定時器的方法檢查依賴的名字空間是否存在。若已經存在,則調用init函數;若不存在,則等待一段時間在檢查。
function init() { // inline code......}var script = document.createElement("script"); script.type = "text/javascript"; script.src = "jquery.js";document.getElementsByTagName(‘head‘)[0].appendChild(script); function timer() { if("undefined" === typeof(jQuery)) { setTimeout(timer,500); } else { init(); }}timer();
缺點:
如果setTimeout設定的時間間隔過小,則可能會增加頁面的開銷;如果時間間隔過大,就會發生外部指令碼載入完畢而行內指令碼需要間隔一段才能時間執行的狀況,從而造成浪費。
如果外部指令碼(jquery.js)載入失敗,則這個輪詢將會一直持續下去。
增加維護成本,因為我們需要通過外部指令碼的特定標識符來判斷指令碼是否載入完畢,如果外部指令碼的標識符變了,則行內的代碼也需要改變。
推薦指數:2顆星
4、window.onload
我們可以使用window.onload事件來觸發行內代碼的執行,但是這要求外部的指令碼必須在window.onload事件觸發之前下載完畢。
在 無阻塞載入指令碼提到的技術中,IFrame嵌入Script 、動態指令碼元素 、Script Defer 可以滿足這點要求。
1 function init() {2 // inline code......3 }4 if(window.addEventListener) {5 window.addEventListener("load",init,false);6 }7 else if(window.attachEvent) {8 window.attachEvent("onload",init);9 }
缺點:這會阻塞window.onload事件,所以並不是一個很好的辦法;如果頁面中還有很多其他資源(譬片、Flash等),那麼行內指令碼將會順延強制(就算它依賴的外部指令碼一早就載入完了),因為window.onload不會觸發。
推薦指數:3顆星
5、降級使用script
來來來,先看看它什麼樣子:
<script src="jquery.js" type="text/javascript"> $(".button").click(function() { alert("hello"); });</script>
然並卵,目前還沒有瀏覽器可以實現這種方式,一般情況下,外部指令碼(jquery.js)載入成功後,兩個標籤之間的代碼就不會執行了。
但是我們可以改進一下:修改外部指令碼的代碼,讓它在DOM樹種搜尋自己,用innerHTML擷取自己內部的代碼,然後用eval執行,就可以解決問題了。
然後我們在修改一下讓它非同步載入,就變成了這樣:
1 function init() {2 // inline code......3 }4 var script = document.createElement("script"); 5 script.type = "text/javascript"; 6 script.src = "jquery.js";7 script.innerHTML = "init()‘"8 document.getElementsByTagName(‘head‘)[0].appendChild(script);
而在外部指令碼中我們需要添加如下代碼:
1 var scripts = document.getElementsByTagName("script");2 3 for(var i = 0; i < scripts.length;i++) {4 if(-1 != scripts[i].src.indexOf(‘jquery.js‘)) {5 eval(script.innerHTML);6 break;7 }8 }
這樣就大功告成 。然而,缺點也很明顯,我們還是需要修改外部檔案的代碼。
推薦指數:2顆星
內聯指令碼、多個外部指令碼相互依賴
舉個栗子:
內聯指令碼依賴a.js,a.js依賴b.js;
這種情況比較麻煩(好吧,是因為我太菜),簡單介紹一下思路:
確保a.js在b.js之後執行,內聯指令碼在a.js之後執行。
我們可以使用XMLHttpRequest同時非同步擷取兩個指令碼,如果a.js先下載完成,則判斷b.js是否下載完成,如果下載完成則執行,否則等待,a.js執行之後就可以調用內聯指令碼執行了。b.js下載完成之後即可執行。
代碼大概這樣(求指正):
1 function init() { 2 // inline code...... 3 } 4 5 6 var xhrA = new XMLHttpRequest(); 7 var xhrB = new XMLHttpRequest(); 8 var scriptA , scriptB; 9 10 var scriptA = document.createElement("script"); 11 scriptA.type = "text/javascript";12 13 var scriptB = document.createElement("script"); 14 scriptB.type = "text/javascript";15 16 scriptA = scriptB = false;17 18 xhrA.open("GET", "a.js", true); 19 xhrA.send(‘‘);20 xhrA.onreadystatechange = function(){21 if (xhr.readyState == 4){22 if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ 23 scriptA.text = xhr.responseText;24 scriptA = true;25 if(scriptB) {26 document.body.appendChild(scriptA);27 init();28 } 29 } 30 }31 }; 32 33 xhrB.open("GET", "b.js", true); 34 xhrB.send(‘‘);35 xhrB.onreadystatechange = function(){36 if (xhr.readyState == 4){37 if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ 38 scriptB.text = xhr.responseText;39 scriptB = true40 document.body.appendChild(scriptB);41 if(scriptA) {42 document.body.appendChild(scriptA);43 init();44 } 45 } 46 }47 };
四、編寫高效的JavaScript
之前講過了,大家可以猛戳 這裡 看一下。
五、CSS選取器最佳化
1、在談論選取器最佳化之前,我們先簡單介紹一下選取器的類型:
ID選取器 : #id;
類別選取器: .class
標籤選取器: a
兄弟選取器:#id + a
子選取器: #id > a
後代選取器: #id a
通賠選取器: *
屬性選取器: input[type=‘input‘]
偽類和虛擬元素:a:hover , div:after
組合選取器:#id,.class
2、瀏覽器的匹配規則
#abc > a怎麼匹配? 有人可能會以為:先找到id為abc的元素,再尋找子項目為a的元素!!too young,too simple!
其實,瀏覽器時從右向左匹配選擇符的!!!那麼上面的寫法效率就低了:先尋找頁面中的所有a標籤,在看它的父元素是不是id為abc
知道了瀏覽器的匹配規則我們就能儘可能的避免開銷很大的選取器了:
避免通配規則
除了 * 之外,還包括子選取器、後台選取器等。
而它們之間的組合更加逆天,譬如:li *
瀏覽器會尋找頁面的所有元素,然後一層一層地尋找他的祖先,看是不是li,這對可能極大地損耗效能。
不限定ID選取器
ID就是唯一的,不要寫成類似div#nav這樣,沒必要。
不限定class選取器
我們可以進一步細化類名,譬如li.nav 寫成 nav-item
盡量避免後代選取器
通常後代選取器是開銷最高的,如果可以,請使用子選取器代替。
替換子選取器
如果可以,用類別選取器代替子選取器,譬如
nav > li 改成 .nav-item
依靠繼承
瞭解那些屬性可以依靠繼承得來,從而避免重複設定規則。
3、關鍵選擇符
選取器中最右邊的選擇符成為關鍵選擇符,它對瀏覽器執行的工作量起主要影響。
舉個栗子:
div div li span.class-special
乍一看,各種後代選取器組合,效能肯定不能忍。其實仔細一想,瀏覽器從右向左匹配,如果頁面中span.class-special的元素只有一個的話,那影響並不大啊。
反過來看,如果是這樣
span.class-special li div div ,儘管span.class-special很少,但是瀏覽器從右邊匹配,尋找頁面中所有div在層層向上尋找,那效能自然就低了。
4、重繪與迴流
最佳化css選取器不僅僅提高頁面載入時候的效率,在頁面迴流、重繪的時候也可以得到不錯的效果,那麼接下來我們說一下重繪與迴流。
4.1、從瀏覽器的渲染過程談起
解析HTML構建dom樹→構建render樹→布局render樹→繪製render樹
1)構建dom樹
根據獲得的html代碼產生一個DOM樹,每個節點代表一個HTML標籤,根節點是document對象。dom樹種包含了所有的HTML標籤,包括未顯示的標籤(display:none)和js添加的標籤。
2)構建cssom樹
將得到所有樣式(瀏覽器和使用者定義的css)除去不能識別的(錯誤的以及css hack),構建成一個cssom樹
3)cssom和dom結合產生渲染樹,渲染樹中不包括隱藏的節點包括(display:none、head標籤),而且每個節點都有自己的style屬性,渲染樹種每一個節點成為一個盒子(box)。注意:透明度為100%的元素以及visibility:hidden的元素也包含在渲染樹之中,因為他們會影響布局。
4)瀏覽器根據渲染樹來繪製頁面
4.2、重繪(repaint)與迴流(reflow)
1)重繪 當渲染樹中的一部分或者全部因為頁面中某些元素的布局、顯示與隱藏、尺寸等改變需要重新構建,這就是迴流。每個頁面至少會發生一次迴流,在頁面第一次載入的時候發生。在迴流的時候,瀏覽器會使渲染樹中受到影響的部分失效,並重新構造這部分渲染樹,完成迴流後,瀏覽器會重新繪製受影響的部分到螢幕中,該過程成為重繪。
2. 當渲染樹中的一些元素需要更新屬性,而這些屬性不會影響布局,隻影響元素的外觀、風格,比如color、background-color,則稱為重繪。
注意:迴流必將引起重繪,而重繪不一定會引起迴流。
4.3、迴流何時發生:
當頁面配置和幾何屬性改變時就需要迴流。下述情況會發生瀏覽器迴流:
1、添加或者刪除可見的DOM元素;
2、元素位置改變;
3、元素尺寸改變——邊距、填充、邊框、寬度和高度
4、內容改變——比如文本改變或者圖片大小改變而引起的計算值寬度和高度改變;
5、頁面渲染初始化;
6、瀏覽器視窗尺寸改變——resize事件發生時;
4.4、如何影響效能
頁面上任何一個結點觸發reflow,都會導致它的子結點及祖先結點重新渲染。
每次重繪和迴流發生時,瀏覽器會根據對應的css重新繪製需要渲染的部分,如果你的選取器不最佳化,就會導致效率降低,所以最佳化選取器的重要性可見一斑。
六、盡量少用iframe
在寫網頁的時候,我們可能會用到iframe,iframe的好處是它完全獨立於父文檔。iframe中包含的JavaScript檔案訪問其父文檔是受限的。例如,來自不同域的iframe不能訪問其父文檔的Cookie。
開銷最高的DOM元素
通常建立iframe元素的開銷要比建立其它元素的開銷高几十倍甚至幾百倍。
iframe阻塞onload事件
通常我們會希望window.onload事件能夠儘可能觸發,原因如下:
- 我們可能在onload事件處理函數中編寫了用於初始化UI的代碼;
- onload事件觸發時,瀏覽器停止“忙指標”,並向使用者反饋頁面已經準備就緒。
- 部分低版本瀏覽器(IE6、IE7、IE8、Safari3、Safari4、Chrome1、Chrome2等)只有onload事件觸發之後才會觸發unload事件。有時,我們會把一些重要的操作和window的unload事件綁定在一起。例如,減少記憶體泄露的代碼。如果onload花費時間太長,使用者可能會離開頁面,那麼在這些瀏覽器中unload可能就永遠不會執行了。
通常情況下,iframe中的內容對頁面來說不是很重要的(譬如第三方的廣告),我們不應該因為這些內容而延遲window.onload事件的觸發。
綜上,即使iframe是空的,其開銷也會很高,而且他會阻塞onload事件。所以,我們應該儘可能避免iframe的使用。
七、圖片最佳化
在大多數網站中,圖片的大小往往能佔到一半以上,所以最佳化圖片能帶來更好的效果;而且,對圖片的最佳化,還可以實現再不刪減網站功能的條件下實現網站效能的提升。
1、映像格式
GIF
透明:允許二進位類型的透明度,要麼完全透明,要麼不透明。
動畫:支援動畫。動畫由若干幀組成。
無損:GIF是無損的
漸進式掃描:產生GIF時,會使用壓縮來減小檔案大小。壓縮時,漸進式掃描像素,當映像在水平方向有很多重複顏色時,可以獲得更好的壓縮效果。
支援隔行掃描
GIF有256色限制,所以不適合顯示照片。可以用來顯示圖形,但是PNG8是用來顯示圖形的最佳方式。所以,一般在需要動畫時才用到GIF。
JPEG
有損
不支援動畫和透明
支援隔行掃描
PNG
透明:PNG支援完全的alpha透明
動畫:目前無跨瀏覽器解決方案
無損
漸進式掃描:和GIF類似,對水平方向有重複顏色的映像壓縮比高。
支援隔行掃描
隔行掃描是什麼:
網速很慢時,部分映像支援對那些連續採樣的映像進行隔行掃描。隔行掃描可以讓使用者在完整下載映像之前,可以先看到映像的一個粗略的版本,從而消除頁面被消極式載入的感覺。
2、PNG在IE6中的奇怪現象
所有在調色盤PNG中的半透明像素在IE6下會顯示為完整的透明。
真彩色PNG中的alpha透明像素,會顯示為背景色
3、無損映像最佳化
PNG映像最佳化
PNG格式映像資訊儲存在”塊“中,對於Web現實來說,大部分塊並非必要,我們可以將其刪除。
推薦工具:Pngcrush
JPEG映像最佳化
剝離中繼資料(注釋、其他內部資訊等)
這些中繼資料可以安全刪除不會影響圖片品質。
推薦工具jpegtran
GIF轉換成PNG
前面提到GIF的功能吃了動畫之外,完全可以用PNG8來代替,所以我們使用PNG代替GIF
推薦工具ImageMagick
最佳化GIF動畫
因為動畫裡面有很多幀,並且部分內容在很多幀上都是一樣的,所以我們可以將映像裡面連續幀中的重複像素移除。
推薦工具:Gifsicle
4、CSS sprite最佳化
如果網站頁面較少,可以將映像放在一個超級CSS sprite中
看看Google就使用了一個:
最佳實務:
- 按照顏色合并:顏色相近的突變組合在一起
- 避免不必要的空白
- 元素水平排列:比豎直排列稍微小點
- 將顏色限制在25種之內(盡量)
- 先最佳化單獨的映像,再最佳化Sprite
- 通過控制大小和對齊減少反鋸齒的數量。
- 避免使用對角線漸層,這種漸層無法被平鋪。
- IE6中alpha透明映像單獨使用sprite
- 每2-3個像素改變漸層顏色,而不是每個
- 避免對映像縮放
- 如果我們需要一張小的映像,就沒必要在下載一張大的映像之後在HTML中將其縮小。
- 譬如我們需要一個100*100的映像,我們可以現在伺服器端改變映像的大小,這樣可以節省下載的流量。
5、避免對映像縮放
如果我們在頁面中用不到大的映像,就沒必要下載一個很大的然後用css限制他的大小。
譬如我們需要一個100*100的映像,我們可以現在伺服器端改變映像的大小,這樣可以節省下載的流量。
八、劃分主域
在之前我們談到為了減少DNS的尋找,我們應該減少域的數量。但有的時候增加域的數量反而會提高效能,關鍵是找到提升效能的關鍵路徑。如果一個域提供了太多的資源而成為關鍵路徑,那麼將資源分派到多個域上(我們成為域劃分),可以使頁面載入更快。
當單個域下載資源成為瓶頸時,可將資源分派到多個域上。通過並行的下載數來提高頁面速度。
譬如YouTube序列化網域名稱:i1.ytimg.com、i2.ytimg.com、i3.ytimg.com、i4.ytimg.com
IP地址和主機名稱
瀏覽器執行“每個服務端最大串連數”的限制是根據URL上的主機名稱,而不是解析出來的IP地址。因此,我們可以不必額外部署伺服器,而是為新域建立一條CNAME記錄。CNAME僅僅是網域名稱的別名,即使網域名稱都指向同一個伺服器,瀏覽器依舊會為每個主機名稱開放最大串連數。
譬如,我們為www.abc.com建立一個別名abc.com,這兩個主機名稱有相同的IP地址,瀏覽器會將每個主機名稱當做一個單獨的服務端。
另外,研究表明,域的數量從一個增加到兩個效能會得到提高,但超過兩個時就可能出現負面影響了。最終數量取決於資源的大小和數量,但分為兩個域是很好的經驗。
Web前端效能最佳化進階——完結篇