網頁性能優化的目的
- 減少服務器壓力
- 增強用戶體驗,減少加載時間,通俗地說就是用戶感覺打開你的網頁很快,等待頁面資源加載的時間很短
方法
回答這題的思路要從另一道面試題:從敲回車開始到頁面展現這個過程發生了什么入手。以下是其中的一些過程:
- 看請求的html頁面有沒緩存,沒有就進行下面步驟
- dns查詢
- 建立TCP連接
- 發送HTTP請求
- 后臺處理
- 接收響應
- 接收完成
- 讀取html的DOCTYPE
- 逐行解析
- 看到
<link href='#'>,<script src='#'>
就會像服務器發起請求
優化以上每個步驟,這樣子整個web網頁的性能就提高了
減少dns查詢
你在瀏覽器地址欄輸入網址,通過DNS查詢得到網站真實IP。
比如你的一個網站,需要向 baidu.com 請求一個js文件,向 bootcdn.cn 請求一個css文件,這樣子就要進行2次dns查詢(沒有dns緩存的情況下),解決方法就是各種資源放在數量盡量少的域名下
域名解析的流程
- 瀏覽器緩存 – 瀏覽器會緩存DNS記錄一段時間
- 系統緩存 - 從 Hosts 文件查找是否有該域名和對應 IP。
- 路由器緩存 – 一般路由器也會緩存域名信息。
- ISP DNS 緩存 – 比如到電信的 DNS 上查找緩存。
- 如果都沒有找到,則向根域名服務器查找域名對應 IP,根域名服務器把請求轉發到下一級,知道找到 IP
把資源分散到不同的域名。
把資源分散到不同的域名允許你最大化并行下載數
例如chrome,只能同時向一個域名最多發8個請求,把資源分散到不同的域名允許你最大化并行下載數,但這樣子dns查詢時間就變長,和前面的“減少dns查詢“沖突了
如果文件很少,例如只有3個文件,放在1個域名下,這樣dns查詢時間就很快,此時如果把這3個文件分散到更多的域名下,速度也不會提高,因為3個請求還沒有達到chrome的只能同時向一個域名最多發請求的數量的極限
如果文件很多,例如16個文件,還全部放在1個域名下,雖然dns查詢時間很快,但請求時間會很慢,因為只能同時向一個域名最多發8個請求,那另外8個請求要等前面的請求發送完才能再發送
懶加載和預加載
懶加載就是例如打開一個網頁,viewport以下的網頁內容先不加載,等到用戶滾動看到那部分內容再加載相應的資源,好處:節省CDN費用,降低服務器壓力
預加載就是預先加載用戶即將要看到的內容,例如當你在第一頁時就預先加載第二頁的內容,這樣子用戶打開第二頁的時候就不用等待加載時間
使用CDN
cdn即內容分發網絡,例如谷歌的主服務器1在美國,在北京有個服務器2,服務器2不斷地把主服務器1上的內容復制過來(可能說得不是很準確),這樣子在北京訪問谷歌,直接訪問北京的服務器,比你訪問美國的主服務器要快
傳輸時gzip壓縮資源
從HTTP/1.1
開始,客戶端通過http請求中的Accept-Encoding
頭部來提示支持的壓縮:
Accept-Encoding: gzip, deflate
如果服務器看到這個頭部,它可能會選用列表中的某個方法壓縮要請求的資源。服務器通過響應中的Content-Encoding
頭部提示客戶端:
Content-Encoding: gzip
客戶端接收到響應后會解壓縮
gzip一般可減小響應的70%。盡可能去gzip更多(文本)類型的文件。html,腳本,樣式,xml和json等等都應該被gzip,而圖片,pdf等等不應該被gzip,因為它們本身已被壓縮過,gzip它們只是浪費cpu,甚至增加文件大小。
Cache-Control
//后臺代碼
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指當第一次請求并下載/js/main.js后,30內遇到同樣/js/main.js的請求(例如刷新當前頁面),會從緩存里直接讀取main.js,不會再向服務器發請求
ETag
在典型用法中,當一個URL被請求,Web服務器會返回資源和其相應的ETag值,它會被放置在HTTP的“ETag”字段中:
ETag: "686897696a7c876b7e"
然后,客戶端可以決定是否緩存這個資源和它的ETag。以后,如果客戶端想再次請求相同的URL,將會發送一個包含已保存的ETag和“If-None-Match”字段的請求。
If-None-Match: "686897696a7c876b7e"
客戶端請求之后,服務器可能會比較客戶端的ETag和當前版本資源的ETag。如果ETag值匹配,這就意味著資源沒有改變,服務器便會發送回一個極短的響應,包含HTTP “304 未修改”的狀態。304狀態告訴客戶端,它的緩存版本是最新的,并應該使用它。
然而,如果ETag的值不匹配,這就意味著資源很可能發生了變化,那么,一個完整的響應就會被返回,包括資源的內容,就好像ETag沒有被使用。這種情況下,客戶端可以用新返回的資源和新的ETag替代先前的緩存版本。
部分的后臺Node.js示例代碼:
上圖代碼的md5(string)能輸出string的md5值,用的是https://www.npmjs.com/package/md5 ,170k指的是string的大小
用沒有cookie的域名提供資源
當瀏覽器請求靜態圖片并把cookie一起發送到服務器時,cookie此時對服務器沒什么用處。所以這些cookie只是增加了網絡流量。所以你應該保證靜態資源的請求是沒有cookie的??梢詣摻ㄒ粋€子域名來托管所有靜態組件。
比如,你域名是www.example.org
,可以把靜態資源托管在static.example.org
。不過,你如果把cookie設置在頂級域名example.org
下,這些cookie仍然會被傳給static.example.org
。這種情況下,啟用一個全新的域名來托管靜態資源
另外一個用沒有cookie的域名提供資源的好處是,某些代理可能會阻止緩存待cookie的靜態資源請求。
減少cookie體積
后端代碼通過 Set-Cookie 響應頭設置 Cookie的內容
瀏覽器得到 Cookie 之后,每次訪問相同域名的網頁時, 每次請求的header都會帶上 Cookie,例如
Cookie:sessionId=44438.40790818172
http cookie的使用有多種原因,比如授權和個性化。cookie的信息通過http頭部在瀏覽器和服務器端交換。盡可能減小cookie的大小來降低響應時間。
- 消除不必要的cookie。
- 盡可能減小cookie的大小來降低響應時間。
- 注意設置cookie到合適的域名級別,則其它子域名不會被影響。
選擇<link>
而不是@import
。
之前的一個最佳原則是說CSS應該在頂部來允許逐步渲染。
在IE用@import
和把CSS放到頁面底部行為一致,所以最好別用
減少/最小化 http 請求數。
有以下幾個技術:
- Combined files。合并文件,如合并js,合并css都能減少請求數。如果頁面間腳本和樣式差異很大,合并會更具挑戰性。
- CSS Sprites。雪碧圖可以合并多個背景圖片,通過
background-image
和background-position
來顯示不同部分。 - Inline images。使用
data:url scheme
來內連圖片。
使用外部JS和CSS。
真實世界中使用外部文件一般會加快頁面,因為JS和CSS文件被瀏覽器緩存了。內連的JS和CSS怎在每次HTML文檔下載時都被下載。內連減少了http請求,但增加了HTML文檔大小。另一方面,如果JS和CSS被緩存了,那么HTML文檔可以減小大小而不增加HTTP請求。
核心因素,就是JS和CSS被緩存相對于HTML文檔被請求的頻率。盡管這個因素很難被量化,但可以用不同的指標來計算。如果網站用戶每個session有多個pv,許多頁面重用相同的JS和CSS,那么有很大可能用外部JS和CSS更好。
唯一例外是內連更被主頁偏愛,如http://www.yahoo.com/
主頁每個session可能只有少量的甚至一個pv,這時候內連可能更快
對多個頁面的首頁來說,可以通過技術減少(其它頁面的)http請求。在首頁用內連,初始化后動態加載外部文件,接下來的頁面如果用到這些文件,就可以使用緩存了。
壓縮JS和CSS。
壓縮就是刪除代碼中不必要的字符來減小文件大小,從而提高加載速度。當代碼壓縮時,注釋刪除,不需要的空格(空白,換行,tab)也被刪除。
混淆是對代碼可選的優化。它比壓縮更復雜,并且可能產生bug。在對美國top10網站的調查,壓縮可減小21%,而混淆可減小25%。
除了外部腳本和樣式,內連的腳本和樣式同樣應該被壓縮。
使用事件委托
有時候頁面看起來不那么響應(響應速度慢),是因為綁定到不同元素的大量事件處理函數執行太多次。這是為什么要使用事件委托
另外,你不必等到onload
事件來開始處理DOM樹,DOMContentLoaded
更快。大多時候你需要的只是想訪問的元素已在DOM樹中,所以你不必等到所有圖片被下載。
優化圖片
在設計師建好圖片后,在上傳圖片到服務器前你仍可以做些事:
- 檢查gif圖片的調色板大小是否匹配圖片顏色數。
- 可以把gif轉成png看看有沒有變小。除了動畫,gif一般可以轉成png8。
- 運行
pngcrush
或其它工具壓縮png。 - 運行
jpegtran
或其它工具壓縮jpeg。
TCP 連接復用
在 HTTP/1.0 時代,每一個請求都會重新建立一個 TCP 連接,一旦響應返回,就關閉連接。 而建立一個連接,則需要進行三次握手(https的話則是9次握手),這極大的浪費了性能
因此 HTTP/1.1 新增了「keep-alive」功能,當瀏覽器建立一個 TCP 連接時,新的請求可以在上次建立的tcp連接之上發送,連接可以復用。。(現如今大多數瀏覽器默認都是開啟的)
HTTP 2.0 多路復用
HTTP/2.0 時代擁有了「多路復用」功能,意思是: 在一條連接上,我可以同時發起無數個請求,并且響應可以同時返回。
多個連接優化
問題:一個1M的JS文件,如何下載比較快?
- 整個文件一個JS
- 拆分成兩個500M的JS
- 拆分成4個250M的JS
理論上答案是3.這樣可以并行下載4個文件,減少了帶寬的占據,但是增加了3次TCP請求時間,在下載時間和TCP請求時間的取舍上,就要具體情況具體測試分析了。
如果是移動端就選擇方案1,因為移動端對多文件下載不友好
如果選擇第3種方案,那么引生出另一個問題:為什么不分得更多?
答:每個瀏覽器對并行下載有上限。每個域名限制最多同時進行N個下載線程。
其他
<link href=''>
和<style>
放<head>
里面,因為無論放在那里都會阻塞html渲染(等到css文件下載并解析完才會顯示html內容),所以還不如放head里面讓css文件盡早下載
js放body最后,這樣子可以獲取html節點,且能先盡早顯示html頁面
chrome devtool的audits panel可以檢測網站的性能優化情況
參考資源: