在討論具體如何優化以前,先思考一個經典問題,從輸入 URL 到頁面加載完成到底發生了什麼?html
URL 通過 DNS 解析爲 IP 地址,而後與 IP 地址進行 TCP 鏈接,隨後發出 HTTP 請求,服務器處理完請求以後將內容經過 HTTP 發送給客戶端,拿到數據後瀏覽器就開始渲染流程。簡單的說分爲如下幾個步驟:DNS 解析,TCP 鏈接,HTTP 請求,HTTP響應,瀏覽器解析並渲染。react
這個問題解決以後,就能夠從各個層面分析如何作性能優化了。webpack
DNS 預解析是指瀏覽器視圖在用戶訪問連接以前解析域名。那接下來用戶若是確實訪問了該域名,那 DNS 的解析時間將不會有延遲。瀏覽器對網站第一次的域名 DNS 查找流程爲:瀏覽器緩存 => 系統緩存 => 路由器緩存 => ISP DNS 緩存 => DNS 服務器。因此咱們不須要對和當前頁面中同一個域的域名進行預獲取(瀏覽器會緩存解析結果)。web
使用起來也很簡單:json
// 若是須要禁用隱式的 DNS prefetch 設置 content = "off"
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//ha.aa.bb">
複製代碼
先來一個例子你們直觀感覺下二者的差別,能夠看到 HTTP/2 性能有大幅提升。這裏就必須提到其引入的多路複用技術,在這個技術的支持下,同一域名下的全部請求都在一個通道內完成。這也是引入了幀和流的概念,幀是數據最小傳輸單位,且標記了屬於哪一個流,流就是多個數據幀組成的數據流。多路複用就是一個鏈接下存在多個流。而 HTTP/1 中每一個請求都必須建立一個 TCP 鏈接,瀏覽器還限制了同一個域名下的請求數量,當請求資源較多的時候會出現隊頭阻塞。瀏覽器
且 HTTP/2 採用二進制傳輸代替了 HTTP/1 中的文本傳輸,解析更加高效。緩存
緩存是性能優化中性價比很高的一種優化方式,顯著的減小了網絡傳輸帶來的損耗。性能優化
瀏覽器的緩存機制有四種,按優先級排列以下:服務器
當以上都沒有命中資源的時候纔去作網絡請求。babel
Service Worker 是運行在瀏覽器背後的獨立線程,且脫離瀏覽器窗口,所以沒法直接訪問 DOM。也正是獨立的特色,咱們每每能夠經過它實現離線緩存,消息推送和網絡代理等功能。但因爲涉及到網絡代理的,使用 Service Worker 時,傳輸協議必須爲 HTTPS。
使用步驟分爲三步:註冊 Service Worker;監聽 install 事件,並緩存須要的文件;在下次請求的時候經過攔截請求的方式查詢是否存在緩存,存在的話直接讀取緩存文件,不然請求資源。
有一點須要注意的是,當咱們沒有在 Service Worker 命中緩存,須要調用 fetch
函數獲取數據時,瀏覽器會依次根據緩存優先級繼續查找,但此時找到的數據依舊會顯示是從 Service Worker 中獲取的。
Memory Cache 是指內存中的緩存,是速度很是快的一種緩存。可是雖然讀取效率高,其生存時間較短,一旦 Tab 頁關閉,內存中的緩存就被釋放了。可是具體哪部份內容會被緩存並不肯定,須要根據系統內存的具體狀況判斷。
Disk Cache 是指存在硬盤中的緩存,讀取速度較慢,可是時效性較高。即便在跨站點的狀況下,相同地址的資源一旦被緩存下來就不會再次去請求數據。
HTTP/2 中的內容,不太瞭解~~有了解的同窗能夠一塊兒交流下呀
前面提到了各類類型的緩存,可是究竟要不要緩存,怎麼判斷緩存過時時間,這些問題都要從緩存策略中找到答案。
瀏覽器的緩存策略分爲兩種:強緩存和協商緩存。
強緩存的實現依賴 Expires
和 Cache-Control
兩個字段來控制。強緩存表示緩存期間不須要再次請求,返回狀態碼 200。
強緩存的早期實現是靠 Expires
字段,這個字段是一個時間戳,表示在這個事件前的緩存都是有效的。能夠看到這個字段十分依賴本地時間,若是修改客戶端時間可能就會出問題。
HTTP/1.1 出現的 Cache-Control
能夠徹底替代 Expires
並提供更豐富的功能。它提供了不少指令:
指令 | 做用 |
---|---|
public | 表示響應能夠被客戶端和代理服務器緩存 |
private | 表示響應只能被客戶端緩存 |
max-age=30 | 緩存 30 s 後過時 |
s-maxage=30 | 覆蓋 max-age ,但只在代理服務器中生效 |
no-store | 不緩存任何響應 |
no-cache | 資源被緩存可是當即失效,下次會發起請求驗證資源是否過時 |
max-stale=30 | 30s 內及時緩存過時也使用該緩存 |
min-fresh=30 | 但願在 30s 內獲取最新的響應 |
若是緩存過時了或是設置了 no-cache
,則進入協商緩存階段。協商緩存的實現依賴於兩個字段::Etag
以及 Last-modified
。當瀏覽器發起驗證請求資源時,若是資源沒有改動,就返回 304 狀態碼,並更新緩存有效期。
當瀏覽器發起請求時,會帶上 If-Modified-Since
字段,它的值是上次請求資源時 Last-modified
提供的時間戳。服務器再判斷在這個時間戳以後是否有改動。可是這個機制仍是存在弊端的,由於時間戳是以秒爲單位計算的,若是再 1s 內的改動是沒法被感知到的。
Etag
就是爲了解決上述問題出現的,瀏覽器會將上次請求資源返回結果攜帶的 Etag
做爲 If-None-Match
的值發送給服務器,有變更的話就返回新的資源。Etag
的缺陷在於服務器須要有額外的開銷,可能會影響性能。
那當沒有設置緩存策略時,瀏覽器會怎麼辦?一般會取響應頭中的 DATE
減去 Last-modified
值的 10% 做爲緩存時間。
大體瞭解了瀏覽器緩存機制以後,要怎麼利用它們來提升性能呢?這纔是咱們真正要解決的問題。
對於頻繁變更的資源,能夠設置 Cache-Control: no-cache
,讓瀏覽器每次都請求服務器驗證資源是否有效。若是有效,能夠有效減小響應數據大小。
對於一些打包事後的代碼文件,好比 webpack ,一般咱們都會對文件名作哈希處理,通常只要文件改動過了,文件名就會改動。因此對於這類文件,能夠採用強緩存策略,設置較長時間的緩存時間,好比 Cache-Control: max-age=31536000
。
優化 Loader
就拿 Babel 舉例,首先優化 Loader 的文件搜索範圍,合理利用 test
,include
,exclude
;緩存編譯過的文件,下次只須要編譯更改過的代碼便可:
loader: 'babel-loader?cacheDirectory=true'
複製代碼
HappyPack
HappyPack 將 loader 的同步執行轉換爲並行執行
plugins: [
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory'],
// 開啓 4 個線程
threads: 4
})
]
複製代碼
DllPlugin
DllPlugin 將制定的庫提早打包後引入,下次打包時只有當庫版本更新後才須要從新打包。
// 單獨配置在一個文件中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想統一打包的類庫
vendor: ['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必須和 output.library 一致
name: '[name]-[hash]',
// 該屬性須要與 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
})
]
}
// 先執行上面的配置文件生成依賴文件,再使用 DllReferencePlugin 將依賴文件引入項目中
// webpack.conf.js
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest 就是以前打包出來的 json 文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}
複製代碼
按需加載
在開發 SPA 項目時,比較好的一個實踐是,可使用按需加載將每一個路由頁面單獨打包爲一個文件,避免首頁加載文件過大,固然對於大型類庫也一樣適用。
Scope Hoisting
// a.js
export const a = 1
// index.js
export {a} from './a.js'
複製代碼
有以上兩個文件,使用 webpack打包後會變爲:
[
/* 0 */
function (module, exports, require) {
//...
},
/* 1 */
function (module, exports, require) {
//...
}
]
複製代碼
使用了 Scope Hoisting 以後,代碼會盡可能合併到一個函數中,變爲:
[
/* 0 */
function (module, exports, require) {
//...
}
]
複製代碼
能夠看到代碼量會減小不少,咱們能夠經過在 webpack 中配置 optimization.concatenateModules
來開啓 Scope Hoisting。
Tree Shaking
Tree Shaking 用於刪除應用中未被引用的代碼,webpack4 默認開啓了這個功能。
web 應用中圖片幾乎是必不可少的資源,也是十分損耗性能的一個點。圖片優化的最好切入點在於根據業務場景作好圖片選型方案。
JPG
JPG 的特色是有損壓縮,體積小,加載快,但不支持透明。因此一般能夠用於大的背景圖或是輪播圖等色彩豐富的圖片中。不適用於一些矢量圖形或是對比比較鮮明的圖片。
PNG
PNG 的特色是無損壓縮,質量高,體積大。一般用於透明圖片,小 LOGO,或是顏色簡單但對比強烈的圖片。
SVG
SVG 的特色是體積小,不失真,兼容性好。通常頁面上的圖標均可以用 SVG 製做,只是渲染成本較高,對性能可能略有影響。
Base64
Base64 的特色是文本文件,可用於頁面上的小圖標。
WebP
WebP 的特色是支持透明,支持動態圖片,支持有損壓縮和無損壓縮。可是兼容性太差,可是對於兼容 WebP 的瀏覽器能夠儘可能多使用。
CSS
不少時候一些效果能夠直接經過 CSS 實現,這個時候就大膽的放棄使用圖片吧。
預加載
<link rel="preload" href="http://example.com">
複製代碼
若是頁面某些資源可能不會立刻用到,可是但願儘早獲取,可使用預加載。它會強制瀏覽器請求資源,但不會阻塞 onload
事件。預加載能夠下降首屏加載時間,由於能夠將一些不影響首屏但很重要的文件延後加載。
預渲染
<link rel="prerender" href="http://example.com">
複製代碼
預渲染是Chrome中的一項功能,能夠改善用戶可見的頁面加載時間。預渲染由引用頁面中的 <link rel =「prerender">
元素觸發。爲預渲染的URL建立一個隱藏頁面,該頁面將徹底加載全部相關資源,以及執行 JS 文件。若是用戶進入該頁面,則隱藏頁面將被交換到當前選項卡中並使其可見。
懶加載
大多數人應該都接觸過圖片懶加載,實際上就是將不關鍵的資源延後加載。舉個例子,當圖片沒有出如今可視區域內,咱們能夠先統一用一張佔位圖來顯示,將真實的 src
存入自定義屬性中,當進入到到可視區域後,再替換 src
屬性。
懶執行
將某些比較耗時的且不須要在首屏中使用的邏輯延後到使用時再計算,通常用於首屏優化中。
CDN 是指一組分佈在各個地區的服務器。這些服務器存儲數據的副本,所以服務器能夠根據那些服務器距離用戶最近來知足數據的請求。CDN 適合被用於存放靜態資源。