前言javascript
在以前的文章 如何優化網站性能,提升頁面加載速度 中,咱們簡單介紹了網站性能優化的重要性以及幾種網站性能優化的方法(沒有看過的能夠狂戳 連接 移步過去看一下),那麼今天咱們深刻討論如何進一步優化網站性能。css
1、拆分初始化負載html
拆分初始化負載——聽名字以爲高大上,其實否則,土一點將講就是將頁面加載時須要的一堆JavaScript文件,分紅兩部分:渲染頁面所必需的(頁面出來,沒他不行)和剩下的。頁面初始化時,只加載必須的,其他的等會加載。前端
其實在現實生產環境中,對於大部分網站:頁面加載完畢(window.onload觸發)時,已經執行的JavaScript函數只佔到所有加載量的少部分,譬如10%到20%或者更少。
java
注意:這裏所說的頁面加載完畢是指window.onload觸發。window.onload何時出發?當頁面中的內容(包括圖片、樣式、腳本)所有加載到瀏覽器時,纔會觸發window.onload,請與jQuery中$(document).ready做區分。jquery
上面咱們能夠看到大部分JavaScript函數下載以後並未執行,這就形成了浪費。所以,若是咱們可以使用某種方式來延遲這部分未使用的代碼的加載,那想必能夠極大的縮減頁面初始化時候的下載量。跨域
拆分文件 瀏覽器
咱們能夠將原來的代碼文件拆分紅兩部分:渲染頁面所必需的(頁面出來,沒他不行)和剩下的;頁面加載時只加載必須的,剩餘的JavaScript代碼在頁面加載完成以後採用無阻塞下載技術當即下載。安全
須要注意的問題:性能優化
1. 咱們能夠經過某些工具(譬如:Firebug)來得到頁面加載時執行的函數,從而將這些代碼拆分紅一個單獨的文件。那麼問題來了,有些代碼在頁面加載的時候不會執行,可是確實必須的,譬如條件判斷代碼或者錯誤處理的代碼。另外JavaScript的做用域問題是相對比較奇葩的,這些都給拆分形成了很大的困難
2. 關於未定義標識符的錯誤,譬如已加載的JavaScript代碼在執行時,引用了一個被咱們拆分延遲加載的JavaScript代碼中的變量,就會形成錯誤。舉個栗子:
頁面加載完成時用戶點擊了某個按鈕(此時原JavaScript文件被拆分,只下載了頁面加載所必需的的代碼),而監聽此按鈕的代碼尚未被下載(由於這不是頁面加載所必需的,因此在拆分時被降級了),因此點擊就沒有響應或者直接報錯(找不到事件處理函數)。
解決方案:
1. 在低優先級的代碼被加載完成時,按鈕處於不可用狀態(可附帶提示信息);
2. 使用樁函數,樁函數與原函數名字相同,可是函數體爲空,這樣就能夠防止報錯了。當剩餘的代碼加載完成時,樁函數就被原來的同名函數覆蓋掉。咱們能夠作的再狠一點:記錄用戶的行爲(點擊、下拉),當剩餘的代碼加載完成時,再根據記錄調用相應的函數。
2、無阻塞加載腳本
大多數瀏覽器能夠並行下載頁面所須要的組件,然而對於腳本文件卻並不是如此。腳本文件在下載時,在其下載完成、解析執行完畢以前,並不會下載任何其餘的內容。這麼作是有道理的,由於瀏覽器並不知道腳本是否會操做頁面的內容;其次,後面加載的腳本可能會依賴前面的腳本 ,若是並行下載,後面的腳本可能會先下載完並執行,產生錯誤。因此,以前咱們講到了腳本應該儘量放在底部接近</body>的位置,就是爲了儘可能減小整個頁面的影響。
接下來咱們討論幾種技術可使頁面不會被腳本的下載阻塞:
一、Script Defer
<script type="text/javascript" src="file1.js" defer></script>
支持瀏覽器: IE4+ 、Firefox 3.5+以及其它新版本的瀏覽器
defer表示該腳本不打算修改DOM,能夠稍後執行。
二、動態腳本元素
var script = document.createElement ("script"); script.type = "text/javascript"; script.src = "a.js"; document.body.appendChild(script);
用動態建立script標籤的方法不會阻塞其它的頁面處理過程,在IE下還能夠並行下載腳本。
三、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的安全性問題
四、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獲取的內容必須和主頁處於相同的域。
五、Script元素的src屬性
1 var script = document.createElement('script'); 2 script.src = 'http://a.com/a.js' 3 document.body.appendChild(script);
這種方式不會阻塞其它組件,並且容許跨域獲取腳本。
六、IFrame嵌入Script
頁面中的iframe和其它元素是並行下載的,所以能夠利用這點將須要加載的腳本嵌入iframe中。
<iframe src="1.html" frameborder="0" width=0 height="0"></iframe>
注意:這裏是1.html而不是1.js,iframe覺得這是html文件,而咱們則把要加載的腳本嵌入其中。
這種方式要求iframe的請求url和主頁面同域。
3、整合異步腳本
上面咱們介紹瞭如何異步加載腳本,提升頁面的加載速度。可是異步加載腳本也是存在問題的,譬如行內腳本依賴外部腳本里面定義的標識,這樣當內聯的腳本執行的時候外部腳本尚未加載完成,那麼就會發生錯誤。
那麼接下來咱們就討論一下如何實如今異步加載腳本的時候又能保證腳本的可以按照正確的順序執行。
單個外部腳本與內聯腳本
譬如:內聯腳本使用了外部腳本定義的標識符,外部腳本採用異步加載提升加載速度
$(".button").click(function() { alert("hello"); });
<script src="jquery.js"></script>
一、Script Onload
經過Script的onload方法監聽腳本是否加載完成,將依賴外部文件的內聯代碼寫在init函數中,在onload事件函數中調用init函數。
script.onload的支持狀況:
IE六、IE七、IE8不支持onload,能夠用onreadystatechange來代替。
IE九、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用來防止在IE九、IE10已結opera中初始化函數執行兩次。
Script Onload是整合內聯腳本和外部異步加載腳本的首選。
推薦指數:5顆星
二、硬編碼回調
將依賴外部文件的內聯代碼寫在init函數中,修改異步加載的文件,在文件中添加對init函數的調用。
缺點:要修改外部文件,而咱們通常不會修改第三方的插件;缺少靈活性,改變回調接口時,須要修改外部的腳本。
推薦指數:2顆星
三、定時器
將依賴外部文件的內聯代碼寫在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顆星
四、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顆星
五、降級使用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 = true 40 document.body.appendChild(scriptB); 41 if(scriptA) { 42 document.body.appendChild(scriptA); 43 init(); 44 } 45 } 46 } 47 };
4、編寫高效的JavaScript
以前講過了,你們能夠猛戳 這裏 看一下。
5、CSS選擇器優化
一、在談論選擇器優化以前,咱們先簡單介紹一下選擇器的類型:
ID選擇器 : #id;
類選擇器: .class
標籤選擇器: a
兄弟選擇器:#id + a
子選擇器: #id > a
後代選擇器: #id a
通賠選擇器: *
屬性選擇器: input[type='input']
僞類和僞元素:a:hover , div:after
組合選擇器:#id,.class
二、瀏覽器的匹配規則
#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
依靠繼承
瞭解那些屬性能夠依靠繼承得來,從而避免重複設定規則。
三、關鍵選擇符
選擇器中最右邊的選擇符成爲關鍵選擇符,它對瀏覽器執行的工做量起主要影響。
舉個栗子:
div div li span.class-special
乍一看,各類後代選擇器組合,性能確定不能忍。其實仔細一想,瀏覽器從右向左匹配,若是頁面中span.class-special的元素只有一個的話,那影響並不大啊。
反過來看,若是是這樣
span.class-special li div div ,儘管span.class-special不多,可是瀏覽器從右邊匹配,查找頁面中全部div在層層向上查找,那性能天然就低了。
四、重繪與迴流
優化css選擇器不只僅提升頁面加載時候的效率,在頁面迴流、重繪的時候也能夠獲得不錯的效果,那麼接下來咱們說一下重繪與迴流。
4.一、從瀏覽器的渲染過程談起
解析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.二、重繪(repaint)與迴流(reflow)
1)重繪 當渲染樹中的一部分或者所有由於頁面中某些元素的佈局、顯示與隱藏、尺寸等改變須要從新構建,這就是迴流。每一個頁面至少會發生一次迴流,在頁面第一次加載的時候發生。在迴流的時候,瀏覽器會使渲染樹中受到影響的部分失效,並從新構造這部分渲染樹,完成迴流後,瀏覽器會從新繪製受影響的部分到屏幕中,該過程成爲重繪。
2. 當渲染樹中的一些元素須要更新屬性,而這些屬性不會影響佈局,隻影響元素的外觀、風格,好比color、background-color,則稱爲重繪。
注意:迴流必將引發重繪,而重繪不必定會引發迴流。
4.三、迴流什麼時候發生:
當頁面佈局和幾何屬性改變時就須要迴流。下述狀況會發生瀏覽器迴流:
一、添加或者刪除可見的DOM元素;
二、元素位置改變;
三、元素尺寸改變——邊距、填充、邊框、寬度和高度
四、內容改變——好比文本改變或者圖片大小改變而引發的計算值寬度和高度改變;
五、頁面渲染初始化;
六、瀏覽器窗口尺寸改變——resize事件發生時;
4.四、如何影響性能
頁面上任何一個結點觸發reflow,都會致使它的子結點及祖先結點從新渲染。
每次重繪和迴流發生時,瀏覽器會根據對應的css從新繪製須要渲染的部分,若是你的選擇器不優化,就會致使效率下降,因此優化選擇器的重要性可見一斑。
6、儘可能少用iframe
在寫網頁的時候,咱們可能會用到iframe,iframe的好處是它徹底獨立於父文檔。iframe中包含的JavaScript文件訪問其父文檔是受限的。例如,來自不一樣域的iframe不能訪問其父文檔的Cookie。
開銷最高的DOM元素
一般建立iframe元素的開銷要比建立其它元素的開銷高几十倍甚至幾百倍。
iframe阻塞onload事件
一般咱們會但願window.onload事件可以儘量觸發,緣由以下:
一般狀況下,iframe中的內容對頁面來講不是很重要的(譬如第三方的廣告),咱們不該該由於這些內容而延遲window.onload事件的觸發。
綜上,即便iframe是空的,其開銷也會很高,並且他會阻塞onload事件。因此,咱們應該儘量避免iframe的使用。
7、圖片優化
在大多數網站中,圖片的大小每每能佔到一半以上,因此優化圖片能帶來更好的效果;並且,對圖片的優化,還能夠實現再不刪減網站功能的條件下實現網站性能的提高。
一、圖像格式
GIF
透明:容許二進制類型的透明度,要麼徹底透明,要麼不透明。
動畫:支持動畫。動畫由若干幀組成。
無損:GIF是無損的
逐行掃描:生成GIF時,會使用壓縮來減少文件大小。壓縮時,逐行掃描像素,當圖像在水平方向有不少重複顏色時,能夠得到更好的壓縮效果。
支持隔行掃描
GIF有256色限制,因此不適合顯示照片。能夠用來顯示圖形,可是PNG8是用來顯示圖形的最佳方式。因此,通常在須要動畫時纔用到GIF。
JPEG
有損
不支持動畫和透明
支持隔行掃描
PNG
透明:PNG支持徹底的alpha透明
動畫:目前無跨瀏覽器解決方案
無損
逐行掃描:和GIF相似,對水平方向有重複顏色的圖像壓縮比高。
支持隔行掃描
隔行掃描是什麼:
網速很慢時,部分圖像支持對那些連續採樣的圖像進行隔行掃描。隔行掃描可讓用戶在完整下載圖像以前,能夠先看到圖像的一個粗略的版本,從而消除頁面被延遲加載的感受。
二、PNG在IE6中的奇怪現象
全部在調色板PNG中的半透明像素在IE6下會顯示爲完整的透明。
真彩色PNG中的alpha透明像素,會顯示爲背景色
三、無損圖像優化
PNG圖像優化
PNG格式圖像信息保存在」塊「中,對於Web現實來講,大部分塊並不是必要,咱們能夠將其刪除。
推薦工具:Pngcrush
JPEG圖像優化
剝離元數據(註釋、其餘內部信息等)
這些元數據能夠安全刪除不會影響圖片質量。
推薦工具jpegtran
GIF轉換成PNG
前面提到GIF的功能吃了動畫以外,徹底能夠用PNG8來代替,因此咱們使用PNG代替GIF
推薦工具ImageMagick
優化GIF動畫
由於動畫裏面有不少幀,而且部份內容在不少幀上都是同樣的,因此咱們能夠將圖像裏面連續幀中的重複像素移除。
推薦工具:Gifsicle
四、CSS sprite優化
若是網站頁面較少,能夠將圖像放在一個超級CSS sprite中
看看Google就使用了一個:
最佳實踐:
五、避免對圖像縮放
若是咱們在頁面中用不到大的圖像,就不必下載一個很大的而後用css限制他的大小。
譬如咱們須要一個100*100的圖像,咱們能夠如今服務器端改變圖像的大小,這樣能夠節省下載的流量。
8、劃分主域
在以前咱們談到爲了減小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性能優化,就暫且說到這裏了,若是有點用的話,不點一下推薦嗎?