提到應付高流量,第一時間想到的多半是後端技術:Load Balancing、Server Side Caching、Horizontal Scaling…等。身為前端工程師可能會覺得自己幫不上忙。但其實前端能做的事情比想像中多——把自己定位成「後端的節流裝置」,就能在大流量場景中扮演關鍵角色。
把前端當成節流裝置#
平常時候,使用者請求量在伺服器可承受範圍內,前後端的互動不會出問題。但雙 11、搶票等熱門時段,一台伺服器要服務的 client 暴增。我們無法改變使用者人數(流量大本來就是好事),但可以減少 Web App 真正打到 server 的請求數量。
心法 把前端想像成水龍頭外的省水裝置:使用者再多,從前端送往後端的水流(請求)還是有節流空間。
前端可以做的幾件事#
我認為下列五件事,是前端能幫後端「節流」的常見技巧:
- 靜態資源的合併與壓縮
- 盡量減少 HTTP 請求數量
- 各種快取機制
- 善用 CDN
- 避免高頻率的網路請求
前面的章節中,HTTP 快取、Service Worker 快取、CDN、CSS Sprites、檔案壓縮等技巧已經討論過,這些其實都對應上述原則。例如 Medium 在二度造訪時許多資源都能命中快取或 CDN,不必再勞煩 origin server。
SSR 的 result caching#
採用 SSR 架構時,每次請求都得透過 rendering server 渲染頁面。流量一大,這台 server 也會吃不消。實務上常見的做法是在 rendering server 中加一層快取,把 SSR 的執行結果暫存起來,後續同樣的請求就不必重新渲染。
可以使用 redis 也可以用 in-memory 快取。對於 Node.js 環境,node-lru-cache 是一個常用的選擇——LRU 演算法天生擅長控制 cache 容量,避免無上限地消耗記憶體。Next.js 的官方 example 就有對應的範例。
Debounce 與 Throttle#
「避免高頻率發出網路請求」的核心手段就是 debounce 與 throttle。它們的差別在於對事件觸發的處理方式:
Debounce#
Debounce 確保某個耗時操作不會被頻繁觸發。例子:搜尋輸入框會根據文字內容呼叫 API。如果不處理,每打一個字就送一次 request:
洛 -> 發出請求
洛杉 -> 發出請求
洛杉磯 -> 發出請求
洛杉磯湖 -> 發出請求
洛杉磯湖人 -> 發出請求加入 debounce 後,只有當使用者停止輸入超過一段時間(例如 500ms)才會真正打 API:
洛
洛杉
洛杉磯
洛杉磯湖
洛杉磯湖人 (超過 500ms 沒有新輸入,發出請求)對 server 來說,請求量直接從 5 次降到 1 次。
Throttle#
Throttle 則是在「指定時間內至多執行一次」,無論事件被觸發多少次。常見的場景:
- Infinite scroll:使用者快速滑動時,避免 callback 被瘋狂呼叫。
- 按鈕的射擊/攻擊:限制每隔一段時間才能再次觸發動作。
Throttle 與 debounce 同樣可以節流,但行為不同:debounce 強調「靜止之後才動」;throttle 強調「固定節奏執行」。
自己實作#
實務上,Lodash 與 RxJS 都已內建這兩個函式,也可以自己用 setTimeout 寫:
function customDebounce(func, delay) {
let timer = null;
return function debounced(...args) {
const context = this;
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(context, args);
}, delay);
};
}
function handleScroll() {
console.log("Do Something...");
}
window.addEventListener("scroll", customDebounce(handleScroll, 300));function customThrottle(func, limit) {
let lastFunc;
let lastRan;
return function throttled(...args) {
const context = this;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if (Date.now() - lastRan >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
function handleScroll() {
console.log("Do Something");
}
window.addEventListener("scroll", customThrottle(handleScroll, 500));補充說明 Debounce 的核心是「事件觸發時清掉舊的計時器,重設新的計時器」;Throttle 則是「比對與上次執行的時間差,超過 limit 才允許再次執行」。
小結#
要應付大流量,根本還是得從伺服器與系統架構著手。但前端並非完全無能為力——透過快取、CDN、減少請求數量、合併與壓縮資源,再加上 debounce 與 throttle 限制觸發頻率,前端可以實際扮演節流器的角色,為後端分擔不少負擔。
許多今天提到的技巧,其實在系列文前面就已經介紹過了。重點在於在「高流量」的場景下,把它們當成一套整體策略運用,而不只是個別獨立的優化技法。