線上測試 Demo 效果對比
千條資料,657k 響應體,size 減少 99%,介面耗時縮短 94%。
- get 引數攜帶在 url 中有長度限制,僅以post 請求作為測試示例
- 由於作者當前環境上行頻寬大,而作者伺服器使用最低配置的網路,下行頻寬小。故可基本忽略上行耗時。
- size 由最佳化前的 657000B 降低為 310B,減少 656690B,減少 99% 體積
Time 由最佳化前的 3460ms 降低為 193ms,減少 3267ms,減少 94% 耗時(注意,這個是因為作者的下行頻寬小,商用頻寬不會有這麼大的差異)
百條資料,65.9k 響應體,size 減少 99%,介面耗時縮短 70%。
- size 由最佳化前的 65900B 降低為 310B,減少 65590B,減少 99% 體積
Time 由最佳化前的 68ms 降低為 20ms,減少 48ms,減少 70% 耗時
十條資料,7k 響應體,資料量較低,網速影響較大
- size 由最佳化前的 7000B 降低為 310B,減少 6690B,減少 95% 體積
Time 由最佳化前的 86ms 降低為 41ms,減少 45ms,減少 52% 耗時
一條資料,1.2k 響應體,資料量更低存在一定誤差
- 介面返回 size 由 1200B 降為 310B,減少 1200-310=890B,減少 890 / 1200 = 74% 體積
Time 由最佳化前的 32ms 降低為 23ms,減少 9ms,減少 28% 耗時
Demo 介紹
執行邏輯
- 先填寫請求引數(僅支援物件結構),服務端會把請求引數將會作為響應體返回給瀏覽器。在右側欄中顯示返回結果。
- 以此來定製模擬特定體積響應體,測試資料量下的效果。
- 測試前,強制重新整理當前頁面,以清除瀏覽器快取。並取消控制檯的「Disable cache」勾選
功能介紹
- sendGet,傳送 get 請求,當發生兩次相同的請求時,第二次會僅返回 304 狀態碼,size 及 time 對比上一次有明確變化
- sendPost,傳送 post 請求,由於 If-None-Match 本身不會在 get 請求中新增,需要前端自行維護,具體實現邏輯下文將介紹到
- 測試資料量,可動態生產請求引數,快速模擬大資料量的請求情況,最大值支援 10000 條資料。
原理
客戶端向服務端發起 http 請求時,攜帶上次請求結果的特徵值,服務端對比執行結果與請求中的特徵值是否一致,如果特徵值一致,則直接返回 304 給客戶端,告知檔案未改變,客戶端使用本地快取.
- 本次最佳化使用到 http1.1 新增的響應頭 ETag ,及新增的請求頭 f-None-Match實現協商快取。
注意:chrome 有個 bug(不知道是 bug 還是估計設計),get 請求 304 狀態在控制檯中會被轉化為 200,但仍可以透過 network 的 size 列體現快取的效果
實現思路
- 新增一箇中介軟體,在 response 中做一個全域性處理,將響應體做一次 hash,並攜帶在 ETag 響應頭中
若請求頭中存在
if-none-match
請求頭,則與 hash 值進行對比,一致則返回 304,無需返回響應體// Etag 處理的中間層 function responseWithEtag(request, response, responseData) { const etag = crypto.createHash('md5').update(responseData).digest('hex') // 獲取返回引數的 hash 值 response.setHeader("ETag", etag); // 設定 ETag,標識本次響應資訊的 hash 資訊,以對比資訊是否 if (request.headers['if-none-match'] === etag) { // 若本次返回值的 hash 資訊,與請求頭的 if-none-match hash 值一致,則直接返回 304,複用瀏覽器快取的資料,無需要額外返回資料,節省頻寬 console.log('ETag hash match') response.writeHead(304, "Not Modified"); response.end(); return } else { // 無 if-none-match 請求頭,或者返回值內容發生變化,hash 匹配不上 console.log('ETag hash mismatching') response.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }); response.end(responseData); // 返回業務資料 } }
- get 的 ETag 協商快取已由瀏覽器中實現,前端無需額外處理
post 請求本身不支援 ETag 快取(post 請求在設計時專為非冪等介面,但實際上已經很多用於複雜查詢介面),需要前端自行實現快取機制
- 在 post 呼叫完成後,以請求路徑及請求體作為 hash 引數,唯一標識該請求。儲存對應的 ETag 響應頭,及對應的返回值。
- 注意,js 獲取 ETag 請求頭,需要後端開放該請求頭的訪問權,
response.setHeader("Access-Control-Expose-Headers", "ETag"); // 允許暴露ETag響應頭,否則前端無法用 JS 獲取 ETag 頭
- 在發起 post 請求時,查詢該請求是否發起過(以請求路徑及請求體作為 hash 引數唯一標識),若發起過,則在請求頭中自行新增
if-none-match
請求頭
// 由於 post 請求本身不支援 ETag 快取,需要前端自行實現快取機制
const eTagPostMap = {}
function ajax(url, type, reqData) {
return new Promise((resolve, reject) => {
let postReqHash = ''; // 以請求路徑及其引數的 hash 作為 key 進行快取
if (type === 'POST') {
postReqHash = md5.create().update(url + JSON.stringify(reqData)).digest('hex'); // 以請求路徑及其引數的 hash 作為 key 進行快取
}
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
let { status, responseText, responseXML } = xhr;
// 注意,GET 請求的 304 響應頭在 chrome 接受時,給到 JS 會變成 200 狀態碼,且有正常的 responseText
if (status >= 200 && status < 300) {
// POST 請求的響應頭有 ETag,則快取該結果
if (type === 'POST' && xhr.getResponseHeader('ETag')) {
eTagPostMap[postReqHash] = {
content: responseText,
etag: xhr.getResponseHeader('ETag'),
}
}
return resolve(responseText);
}
if (status === 304) { // 注意,GET 請求的 304 響應頭在 chrome 接受時,給到 JS 會變成 200 狀態碼
if (type === 'POST') {
return resolve(eTagPostMap[postReqHash].content);
} else if (type === 'GET') { // 在 chrome 瀏覽器 GET 請求不會有 304 狀態碼,在這裡只是兜底,以防其他瀏覽器表現不一致
return resolve(responseText);
}
}
if (!status) {
alert('get 請求不支援這麼大的請求引數,請減少數量 或 使用 post 請求')
}
reject(status);
}
};
if (type === 'GET') {
xhr.open('GET', url + '?' + serializeParams(reqData), true);
xhr.send(null);
} else if (type === 'POST') {
xhr.open('POST', url, true);
// 如果之前傳送過這個請求,且有 ETag hash 記錄,則當再次發起時,攜帶上該 hash
if (eTagPostMap[postReqHash]) {
xhr.setRequestHeader("if-none-match", eTagPostMap[postReqHash].etag);
}
xhr.send(JSON.stringify(reqData));
}
})
hash 效能
- 因引入了請求體 hash 運算邏輯,需評估其帶來的執行耗時
- 以 1000 條資料 657k,執行 hash 100 次為例,共耗時 180ms,平均單次耗時 1.8ms
對比減少的 3267ms 下行耗時,可忽略不計
風險管控
適用場景
- get,post 冪等的查詢類介面,不應用於非冪等介面
快速切換與回滾
- 後端下發 ETag 響應頭客戶端才會使用快取,整個快取開關的控制全均在後端,若功能出現問題,可實現快速切換恢復。
瀏覽器相容性支援
- 部分魔改的瀏覽器不確實是否支援 http1.1 協議
- 後端可請求頭中的 User-Agent 判斷當前客戶端瀏覽器,先從 chrome 支援開始,再逐步支援其他瀏覽器,或全量支援。
定製特定介面不快取
- 特定介面不下發 ETag 即可關閉該介面的快取功能,實現介面快取的高度定製