網(wǎng)頁(yè)性能優(yōu)化的目的
- 減少服務(wù)器壓力
- 增強(qiáng)用戶體驗(yàn),減少加載時(shí)間,通俗地說(shuō)就是用戶感覺(jué)打開(kāi)你的網(wǎng)頁(yè)很快,等待頁(yè)面資源加載的時(shí)間很短
方法
回答這題的思路要從另一道面試題:從敲回車開(kāi)始到頁(yè)面展現(xiàn)這個(gè)過(guò)程發(fā)生了什么入手。以下是其中的一些過(guò)程:
- 看請(qǐng)求的html頁(yè)面有沒(méi)緩存,沒(méi)有就進(jìn)行下面步驟
- dns查詢
- 建立TCP連接
- 發(fā)送HTTP請(qǐng)求
- 后臺(tái)處理
- 接收響應(yīng)
- 接收完成
- 讀取html的DOCTYPE
- 逐行解析
- 看到
<link href='#'>,<script src='#'>就會(huì)像服務(wù)器發(fā)起請(qǐng)求
優(yōu)化以上每個(gè)步驟,這樣子整個(gè)web網(wǎng)頁(yè)的性能就提高了
減少dns查詢
你在瀏覽器地址欄輸入網(wǎng)址,通過(guò)DNS查詢得到網(wǎng)站真實(shí)IP。
比如你的一個(gè)網(wǎng)站,需要向 baidu.com 請(qǐng)求一個(gè)js文件,向 bootcdn.cn 請(qǐng)求一個(gè)css文件,這樣子就要進(jìn)行2次dns查詢(沒(méi)有dns緩存的情況下),解決方法就是各種資源放在數(shù)量盡量少的域名下
域名解析的流程
- 瀏覽器緩存 – 瀏覽器會(huì)緩存DNS記錄一段時(shí)間
- 系統(tǒng)緩存 - 從 Hosts 文件查找是否有該域名和對(duì)應(yīng) IP。
- 路由器緩存 – 一般路由器也會(huì)緩存域名信息。
- ISP DNS 緩存 – 比如到電信的 DNS 上查找緩存。
- 如果都沒(méi)有找到,則向根域名服務(wù)器查找域名對(duì)應(yīng) IP,根域名服務(wù)器把請(qǐng)求轉(zhuǎn)發(fā)到下一級(jí),知道找到 IP
把資源分散到不同的域名。
把資源分散到不同的域名允許你最大化并行下載數(shù)
例如chrome,只能同時(shí)向一個(gè)域名最多發(fā)8個(gè)請(qǐng)求,把資源分散到不同的域名允許你最大化并行下載數(shù),但這樣子dns查詢時(shí)間就變長(zhǎng),和前面的“減少dns查詢“沖突了
如果文件很少,例如只有3個(gè)文件,放在1個(gè)域名下,這樣dns查詢時(shí)間就很快,此時(shí)如果把這3個(gè)文件分散到更多的域名下,速度也不會(huì)提高,因?yàn)?個(gè)請(qǐng)求還沒(méi)有達(dá)到chrome的只能同時(shí)向一個(gè)域名最多發(fā)請(qǐng)求的數(shù)量的極限
如果文件很多,例如16個(gè)文件,還全部放在1個(gè)域名下,雖然dns查詢時(shí)間很快,但請(qǐng)求時(shí)間會(huì)很慢,因?yàn)橹荒芡瑫r(shí)向一個(gè)域名最多發(fā)8個(gè)請(qǐng)求,那另外8個(gè)請(qǐng)求要等前面的請(qǐng)求發(fā)送完才能再發(fā)送
懶加載和預(yù)加載
懶加載就是例如打開(kāi)一個(gè)網(wǎng)頁(yè),viewport以下的網(wǎng)頁(yè)內(nèi)容先不加載,等到用戶滾動(dòng)看到那部分內(nèi)容再加載相應(yīng)的資源,好處:節(jié)省CDN費(fèi)用,降低服務(wù)器壓力
預(yù)加載就是預(yù)先加載用戶即將要看到的內(nèi)容,例如當(dāng)你在第一頁(yè)時(shí)就預(yù)先加載第二頁(yè)的內(nèi)容,這樣子用戶打開(kāi)第二頁(yè)的時(shí)候就不用等待加載時(shí)間
使用CDN
cdn即內(nèi)容分發(fā)網(wǎng)絡(luò),例如谷歌的主服務(wù)器1在美國(guó),在北京有個(gè)服務(wù)器2,服務(wù)器2不斷地把主服務(wù)器1上的內(nèi)容復(fù)制過(guò)來(lái)(可能說(shuō)得不是很準(zhǔn)確),這樣子在北京訪問(wèn)谷歌,直接訪問(wèn)北京的服務(wù)器,比你訪問(wèn)美國(guó)的主服務(wù)器要快
傳輸時(shí)gzip壓縮資源
從HTTP/1.1開(kāi)始,客戶端通過(guò)http請(qǐng)求中的Accept-Encoding頭部來(lái)提示支持的壓縮:
Accept-Encoding: gzip, deflate
如果服務(wù)器看到這個(gè)頭部,它可能會(huì)選用列表中的某個(gè)方法壓縮要請(qǐng)求的資源。服務(wù)器通過(guò)響應(yīng)中的Content-Encoding頭部提示客戶端:
Content-Encoding: gzip
客戶端接收到響應(yīng)后會(huì)解壓縮
gzip一般可減小響應(yīng)的70%。盡可能去gzip更多(文本)類型的文件。html,腳本,樣式,xml和json等等都應(yīng)該被gzip,而圖片,pdf等等不應(yīng)該被gzip,因?yàn)樗鼈儽旧硪驯粔嚎s過(guò),gzip它們只是浪費(fèi)cpu,甚至增加文件大小。
Cache-Control
//后臺(tái)代碼
if (path ==='/js/main.js') {
let string = fs.readFileSync('./js/main.js', 'utf8');
response.setHeader('Content-Type', 'application/javasript;charset=utf-8');
response.setHeader('Cache-Control', 'max-age=30');
response.write(string);
response.end()
}
max-age=30指當(dāng)?shù)谝淮握?qǐng)求并下載/js/main.js后,30內(nèi)遇到同樣/js/main.js的請(qǐng)求(例如刷新當(dāng)前頁(yè)面),會(huì)從緩存里直接讀取main.js,不會(huì)再向服務(wù)器發(fā)請(qǐng)求
ETag
在典型用法中,當(dāng)一個(gè)URL被請(qǐng)求,Web服務(wù)器會(huì)返回資源和其相應(yīng)的ETag值,它會(huì)被放置在HTTP的“ETag”字段中:
ETag: "686897696a7c876b7e"
然后,客戶端可以決定是否緩存這個(gè)資源和它的ETag。以后,如果客戶端想再次請(qǐng)求相同的URL,將會(huì)發(fā)送一個(gè)包含已保存的ETag和“If-None-Match”字段的請(qǐng)求。
If-None-Match: "686897696a7c876b7e"
客戶端請(qǐng)求之后,服務(wù)器可能會(huì)比較客戶端的ETag和當(dāng)前版本資源的ETag。如果ETag值匹配,這就意味著資源沒(méi)有改變,服務(wù)器便會(huì)發(fā)送回一個(gè)極短的響應(yīng),包含HTTP “304 未修改”的狀態(tài)。304狀態(tài)告訴客戶端,它的緩存版本是最新的,并應(yīng)該使用它。
然而,如果ETag的值不匹配,這就意味著資源很可能發(fā)生了變化,那么,一個(gè)完整的響應(yīng)就會(huì)被返回,包括資源的內(nèi)容,就好像ETag沒(méi)有被使用。這種情況下,客戶端可以用新返回的資源和新的ETag替代先前的緩存版本。
部分的后臺(tái)Node.js示例代碼:

上圖代碼的md5(string)能輸出string的md5值,用的是https://www.npmjs.com/package/md5 ,170k指的是string的大小
用沒(méi)有cookie的域名提供資源
當(dāng)瀏覽器請(qǐng)求靜態(tài)圖片并把cookie一起發(fā)送到服務(wù)器時(shí),cookie此時(shí)對(duì)服務(wù)器沒(méi)什么用處。所以這些cookie只是增加了網(wǎng)絡(luò)流量。所以你應(yīng)該保證靜態(tài)資源的請(qǐng)求是沒(méi)有cookie的??梢詣?chuàng)建一個(gè)子域名來(lái)托管所有靜態(tài)組件。
比如,你域名是www.example.org,可以把靜態(tài)資源托管在static.example.org。不過(guò),你如果把cookie設(shè)置在頂級(jí)域名example.org下,這些cookie仍然會(huì)被傳給static.example.org。這種情況下,啟用一個(gè)全新的域名來(lái)托管靜態(tài)資源
另外一個(gè)用沒(méi)有cookie的域名提供資源的好處是,某些代理可能會(huì)阻止緩存待cookie的靜態(tài)資源請(qǐng)求。
減少cookie體積
后端代碼通過(guò) Set-Cookie 響應(yīng)頭設(shè)置 Cookie的內(nèi)容
瀏覽器得到 Cookie 之后,每次訪問(wèn)相同域名的網(wǎng)頁(yè)時(shí), 每次請(qǐng)求的header都會(huì)帶上 Cookie,例如
Cookie:sessionId=44438.40790818172
http cookie的使用有多種原因,比如授權(quán)和個(gè)性化。cookie的信息通過(guò)http頭部在瀏覽器和服務(wù)器端交換。盡可能減小cookie的大小來(lái)降低響應(yīng)時(shí)間。
- 消除不必要的cookie。
- 盡可能減小cookie的大小來(lái)降低響應(yīng)時(shí)間。
- 注意設(shè)置cookie到合適的域名級(jí)別,則其它子域名不會(huì)被影響。
選擇<link>而不是@import。
之前的一個(gè)最佳原則是說(shuō)CSS應(yīng)該在頂部來(lái)允許逐步渲染。
在IE用@import和把CSS放到頁(yè)面底部行為一致,所以最好別用
減少/最小化 http 請(qǐng)求數(shù)。
有以下幾個(gè)技術(shù):
- Combined files。合并文件,如合并js,合并css都能減少請(qǐng)求數(shù)。如果頁(yè)面間腳本和樣式差異很大,合并會(huì)更具挑戰(zhàn)性。
- CSS Sprites。雪碧圖可以合并多個(gè)背景圖片,通過(guò)
background-image和background-position來(lái)顯示不同部分。 - Inline images。使用
data:url scheme來(lái)內(nèi)連圖片。
使用外部JS和CSS。
真實(shí)世界中使用外部文件一般會(huì)加快頁(yè)面,因?yàn)镴S和CSS文件被瀏覽器緩存了。內(nèi)連的JS和CSS怎在每次HTML文檔下載時(shí)都被下載。內(nèi)連減少了http請(qǐng)求,但增加了HTML文檔大小。另一方面,如果JS和CSS被緩存了,那么HTML文檔可以減小大小而不增加HTTP請(qǐng)求。
核心因素,就是JS和CSS被緩存相對(duì)于HTML文檔被請(qǐng)求的頻率。盡管這個(gè)因素很難被量化,但可以用不同的指標(biāo)來(lái)計(jì)算。如果網(wǎng)站用戶每個(gè)session有多個(gè)pv,許多頁(yè)面重用相同的JS和CSS,那么有很大可能用外部JS和CSS更好。
唯一例外是內(nèi)連更被主頁(yè)偏愛(ài),如http://www.yahoo.com/
主頁(yè)每個(gè)session可能只有少量的甚至一個(gè)pv,這時(shí)候內(nèi)連可能更快
對(duì)多個(gè)頁(yè)面的首頁(yè)來(lái)說(shuō),可以通過(guò)技術(shù)減少(其它頁(yè)面的)http請(qǐng)求。在首頁(yè)用內(nèi)連,初始化后動(dòng)態(tài)加載外部文件,接下來(lái)的頁(yè)面如果用到這些文件,就可以使用緩存了。
壓縮JS和CSS。
壓縮就是刪除代碼中不必要的字符來(lái)減小文件大小,從而提高加載速度。當(dāng)代碼壓縮時(shí),注釋刪除,不需要的空格(空白,換行,tab)也被刪除。
混淆是對(duì)代碼可選的優(yōu)化。它比壓縮更復(fù)雜,并且可能產(chǎn)生bug。在對(duì)美國(guó)top10網(wǎng)站的調(diào)查,壓縮可減小21%,而混淆可減小25%。
除了外部腳本和樣式,內(nèi)連的腳本和樣式同樣應(yīng)該被壓縮。
使用事件委托
有時(shí)候頁(yè)面看起來(lái)不那么響應(yīng)(響應(yīng)速度慢),是因?yàn)榻壎ǖ讲煌氐拇罅渴录幚砗瘮?shù)執(zhí)行太多次。這是為什么要使用事件委托
另外,你不必等到onload事件來(lái)開(kāi)始處理DOM樹(shù),DOMContentLoaded更快。大多時(shí)候你需要的只是想訪問(wèn)的元素已在DOM樹(shù)中,所以你不必等到所有圖片被下載。
優(yōu)化圖片
在設(shè)計(jì)師建好圖片后,在上傳圖片到服務(wù)器前你仍可以做些事:
- 檢查gif圖片的調(diào)色板大小是否匹配圖片顏色數(shù)。
- 可以把gif轉(zhuǎn)成png看看有沒(méi)有變小。除了動(dòng)畫(huà),gif一般可以轉(zhuǎn)成png8。
- 運(yùn)行
pngcrush或其它工具壓縮png。 - 運(yùn)行
jpegtran或其它工具壓縮jpeg。
TCP 連接復(fù)用
在 HTTP/1.0 時(shí)代,每一個(gè)請(qǐng)求都會(huì)重新建立一個(gè) TCP 連接,一旦響應(yīng)返回,就關(guān)閉連接。 而建立一個(gè)連接,則需要進(jìn)行三次握手(https的話則是9次握手),這極大的浪費(fèi)了性能
因此 HTTP/1.1 新增了「keep-alive」功能,當(dāng)瀏覽器建立一個(gè) TCP 連接時(shí),新的請(qǐng)求可以在上次建立的tcp連接之上發(fā)送,連接可以復(fù)用。。(現(xiàn)如今大多數(shù)瀏覽器默認(rèn)都是開(kāi)啟的)
HTTP 2.0 多路復(fù)用
HTTP/2.0 時(shí)代擁有了「多路復(fù)用」功能,意思是: 在一條連接上,我可以同時(shí)發(fā)起無(wú)數(shù)個(gè)請(qǐng)求,并且響應(yīng)可以同時(shí)返回。
多個(gè)連接優(yōu)化
問(wèn)題:一個(gè)1M的JS文件,如何下載比較快?
- 整個(gè)文件一個(gè)JS
- 拆分成兩個(gè)500M的JS
- 拆分成4個(gè)250M的JS
理論上答案是3.這樣可以并行下載4個(gè)文件,減少了帶寬的占據(jù),但是增加了3次TCP請(qǐng)求時(shí)間,在下載時(shí)間和TCP請(qǐng)求時(shí)間的取舍上,就要具體情況具體測(cè)試分析了。
如果是移動(dòng)端就選擇方案1,因?yàn)橐苿?dòng)端對(duì)多文件下載不友好
如果選擇第3種方案,那么引生出另一個(gè)問(wèn)題:為什么不分得更多?
答:每個(gè)瀏覽器對(duì)并行下載有上限。每個(gè)域名限制最多同時(shí)進(jìn)行N個(gè)下載線程。
其他
<link href=''>和<style>放<head>里面,因?yàn)闊o(wú)論放在那里都會(huì)阻塞html渲染(等到css文件下載并解析完才會(huì)顯示html內(nèi)容),所以還不如放head里面讓css文件盡早下載
js放body最后,這樣子可以獲取html節(jié)點(diǎn),且能先盡早顯示html頁(yè)面
chrome devtool的audits panel可以檢測(cè)網(wǎng)站的性能優(yōu)化情況
參考資源: