前言
在之前的文章 如何優化網站性能,提高頁面加載速度 中,我們簡單介紹了網站性能優化的重要性以及幾種網站性能優化的方法(沒有看過的可以狂戳 鏈接 移步過去看一下),那麼今天我們深入討論如何進一步優化網站性能。
一、拆分初始化負載
拆分初始化負載——聽名字覺得高大上,其實不然,土一點將講就是將頁面加載時需要的一堆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解析執行。
var xhr = getXHRObj(); xhr.onreadystatechange = function() { if(xhr.readyState == 4 && xhr.status == 200) { eval(xhr.responseText); } }; xhr.open('GET','text.js',true); xhr.send(''); function getXHRObj() { // ...... return xhrObj; }
該方式不會阻塞頁面中其它組件的下載。
缺點:(1)腳本的域必須和主頁面在相同的域中;(2)eval的安全性問題
4、XHR Injection
XMLHttpRequest Injection(XHR腳本注入)和XHR Eval類似,都是通過 XMLHttpRequest 來獲取JavaScript的。 在獲得文件之後 ,將會創建一個script標簽將得到的代碼注入頁面。
var xhr = new XMLHttpRequest(); xhr.open("GET", "test.js", true); xhr.send(''); xhr.onreadystatechange = function(){ if (xhr.readyState == 4){ if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ var script = document.createElement("script"); script.type = "text/javascript"; script.text = xhr.responseText; document.body.appendChild(script); } } };
XMLHttpRequest獲取的內容必須和主頁處於相同的域。
5、Script元素的src屬性
var script = document.createElement('script'); script.src = 'http://a.com/a.js' 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均有效。
function init() { // inline code...... } var script = document.createElement("script"); script.type = "text/javascript"; script.src = "a.js"; script.onloadDone = false; script.onreadystatechange = function(){ if((script.readyState == 'loaded' || script.readyState == 'complete') && !script.onloadDone){ // alert("onreadystatechange"); init(); } } script.onload = function(){ // alert("onload"); init(); script.onloadDone = true; } 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 可以滿足這點要求。
function init() { // inline code...... } if(window.addEventListener) { window.addEventListener("load",init,false); } else if(window.attachEvent) { window.attachEvent("onload",init); }
缺點:這會阻塞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執行,就可以解決問題了。
然後我們在修改一下讓它異步加載,就變成了這樣:
function init() { // inline code...... } var script = document.createElement("script"); script.type = "text/javascript"; script.src = "jquery.js"; script.innerHTML = "init()'" document.getElementsByTagName('head')[0].appendChild(script);
而在外部腳本中我們需要添加如下代碼:
var scripts = document.getElementsByTagName("script"); for(var i = 0; i < scripts.length;i++) { if(-1 != scripts[i].src.indexOf('jquery.js')) { eval(script.innerHTML); break; } }
這樣就大功告成 。然而,缺點也很明顯,我們還是需要修改外部文件的代碼。
推薦指數:2顆星
內聯腳本、多個外部腳本相互依賴
舉個栗子:
內聯腳本依賴a.js,a.js依賴b.js;
這種情況比較麻煩(好吧,是因為我太菜),簡單介紹一下思路:
確保a.js在b.js之後執行,內聯腳本在a.js之後執行。
我們可以使用XMLHttpRequest同時異步獲取兩個腳本,如果a.js先下載完成,則判斷b.js是否下載完成,如果下載完成則執行,否則等待,a.js執行之後就可以調用內聯腳本執行了。b.js下載完成之後即可執行。
代碼大概這樣(求指正):
function init() { // inline code...... } var xhrA = new XMLHttpRequest(); var xhrB = new XMLHttpRequest(); var scriptA , scriptB; var scriptA = document.createElement("script"); scriptA.type = "text/javascript"; var scriptB = document.createElement("script"); scriptB.type = "text/javascript"; scriptA = scriptB = false; xhrA.open("GET", "a.js", true); xhrA.send(''); xhrA.onreadystatechange = function(){ if (xhr.readyState == 4){ if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ scriptA.text = xhr.responseText; scriptA = true; if(scriptB) { document.body.appendChild(scriptA); init(); } } } }; xhrB.open("GET", "b.js", true); xhrB.send(''); xhrB.onreadystatechange = function(){ if (xhr.readyState == 4){ if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ scriptB.text = xhr.responseText; scriptB = true document.body.appendChild(scriptB); if(scriptA) { document.body.appendChild(scriptA); init(); } } } };
四、編寫高效的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/3、編寫高效的css選擇器
知道了浏覽器的匹配規則我們就能盡可能的避免開銷很大的選擇器了:
避免通配規則
除了 * 之外,還包括子選擇器、後台選擇器等。
而它們之間的組合更加逆天,譬如: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事件能夠盡可能觸發,原因如下:
通常情況下,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就使用了一個:
最佳實踐:
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地址,浏覽器會將每個主機名當做一個單獨的服務端。
另外,研究表明,域的數量從一個增加到兩個性能會得到提高,但超過兩個時就可能出現負面影響了。最終數量取決於資源的大小和數量,但分為兩個域是很好的經驗。
如果是原創文章,轉載注明出處http://www.cnblogs.com/MarcoHan/
之前講了兩篇關於Web性能優化的文章,Web前端性能優化——編寫高效的JavaScript 和Web前端性能優化——如何提高頁面加載速度。那麼關於Web性能優化,就暫且說到這裡了,如果有點用的話,不點一下推薦嗎?