iT邦幫忙

2021 iThome 鐵人賽

DAY 29
1
Modern Web

今晚,我想來點 Web 前端效能優化大補帖!系列 第 29

Day29 X 面對高流量,前端可以做些什麼?

在現今的 Web 應用中,要建構一個穩定的大型系統,能夠處理 High Concurrency 的流量是一個不可或缺的條件,尤其是在服務的熱門時段,例如優惠活動、搶票...等等,都在考驗系統的效能與穩定性。

提到應付高流量,會馬上想到的通常都是一些 Server Side 的技術,例如 Load Balancing、Server Side Caching、Horizontal Scaling...等等。這時身為前端開發者的你也許會感到ㄧ絲沮喪,感覺自己在面對大流量下一點忙也幫不上,有種突然被推上工程師鄙視鏈頂端的感覺!?(誤?)

今天的文章相比於前面的章節也許會稍微短了一些,不過我認為是個蠻有趣的主題,來看看面對高流量的使用者請求,身為前端開發者,我們可以做些什麼吧!

作為後端的節流裝置

不能否認大部分的高流量應對策略都必須依靠後端的實作,例如開多台機器來作分流,資料庫採用讀寫分離架構來分散壓力。身為前端開發者,我們許多的優化技巧主要聚焦於如何在從伺服器端拿到網頁相關資源後盡量降低頁面的渲染時間、盡量達到較好的效能指標(例如 TTI, FCP 這些指標)並提供良好的使用者體驗,又或者像是頁面流暢度這種跟個別使用者比較有關的體驗。許多跟前端相關的優化策略,像前幾天介紹的 HTTP2 也得靠後端的支援才能實作。不過我們可以先換個角度思考,造成後端伺服器必須處理高流量的原因是什麼?我想其中一個原因可以歸咎於:client side 在短時間內同時發了大量請求到 server side。

正常情況下,使用者與前後端溝通的狀況應該為以下這樣:

流量都還在 server 可以承受的範圍內。但假設今天是雙11,一個電商平台的流量狀況可能會變成這樣:

面對高流量,可能會對 server 造成不小的壓力,既然使用者很多,圖片中 Users 到 Web App 這段的流量數基本上沒辦法改變(甚至是一個好的象徵,代表你的產品有人要用啊),那我們可以更聚焦在 Web App 到 Server 這段,有沒有可能減少 Web App 對 Server 的請求數量呢?像是以下這個狀況:

這個概念有點像是把 Web App 這層看成水龍頭,在水龍頭外裝一個省水的節流裝置,限制前端 Web App 向後端 server 發出的請求數量,使 server 即使在面對大量的使用者的狀況下,實際上不用處理那麼多的請求,以防止無法負荷的狀況。於是身為前端開發者,我們就成為了後端的節流器,負責盡量減少真實傳送到後端的網路請求。

How To Do That ?

這似乎是一個不錯的策略,身為前端開發者,面對大流量我們果然還是能幫到後端的忙嘛!不過看起來好像是一門深奧的技術,很難做到的感覺。你想太多囉,其實需要的技巧都在系列文介紹過囉!在面對 high concurrency 的狀況下,我認為前端可以透過以下幾種技巧來幫助後端節流:

  • 靜態資源的合併與壓縮
  • 盡量減少 HTTP 請求數量
  • 各種 Cache 機制
  • CDN
  • 避免高頻率的發出網路請求

前端快取機制(包含 HTTP cache, service worker cache, cookie, localstorage, sessionstorage)與 CDN 都在前面的篇章有詳細提過了,因此就不再贅述。

以 Medium 為例,經過快取機制後,再次造訪頁面時許多資源都可以直接到瀏覽器的快取或是 CDN 拿取,而不用再回到 Origin Server 抓取,可以為伺服器減少一些流量負擔。

如果是採用 Server Side Rendering 的架構的話,通常會有一個 Node.js 的 rendering server 負責在 server side 產生 render 後的結果,並送到前端進行 hydration。

因為每一次 SSR 請求都需要經過這個 rendering server,當流量一大也會對這個 server 造成不小的負擔,所以可以在這個 rendering server 中建立一個 memory 的 cache(要用 redis 也可以,不過設置上比較麻煩一點)來快取 SSR 的執行結果,就不用每一次有 SSR 請求都在 server side 重新渲染。而在這個需求下 LRU Cache 就是一個蠻適合的快取機制,因為 rendering server 通常是 Node.js 的環境,因此可以使用 node-lru-cache 這個 npm 套件,至於 LRU 這個演算法的實作細節與概念我就不在這贅述,有興趣的讀者再自行研究囉。

以上就是基本的 SSR result Caching,也建議可以到 Next.js repo 上的 example 看看。

至於「靜態資源的合併與壓縮」還有「盡量減少 HTTP 請求數量」這兩點看似抽象陌生,其實系列文中的某些優化技巧運用的便是這些原則,重點在於降低請求的「大小」與「數量」。例如 Day07 介紹的 CSS Sprites 技術就是運用減少請求數量的概念,Day16 介紹的檔案壓縮則是降低請求的大小的概念。

Debounce & Throttle

而關於「避免高頻率的發出網路請求」,應該許多開發者都有聽過 throttle 與 debounce 的概念,因為前面的篇章沒有提過,所以今天主要會聚焦在這兩個概念上。

Debounce

Debounce 確保耗時的任務不會被頻繁觸發,從而導致網頁性能停滯。
Debounce 是讓使用者在觸發相同事件(以前端來說像是點擊、輸入)的情境下,停止觸發綁定事件的效果,直到使用者停止觸發相同事件。換句話說,它限制了函數被執行的速率。

聽起來有點抽象,舉一個最常見的例子。假設有一個輸入匡,會根據使用者輸入的內容來發出網路請求透過 API 來抓取相關的資料,實作可能會是這樣

監聽 input element 的 change 事件並發出 AJAX request。
不過這樣有一個問題,就是每一次 iuput 每打一個字都會發一次請求,然而通常在輸入完整的句子或是有意義的單字出現前,這些不完整的字串發出請求後拿到的結果通常對我們來說都沒什麼意義,例如今天我想要查我最愛的 NBA 球隊「洛杉磯湖人」

洛 -> 發出請求
洛杉 -> 發出請求
洛杉磯 -> 發出請求
洛杉磯湖 -> 發出請求
洛杉磯湖人 -> 發出請求

就發出了五次網路請求,但可能至少到 「洛杉磯湖」的時候才會有我想要的資訊(畢竟洛杉磯很大,你光打洛杉磯可能會跳出一堆跟 NBA 無關的東西),這時候就可以運用 debounce 的機制,設定一個時間區段,當使用者在輸入後過了該段時間仍然沒有下一次輸入事件時,才會呼叫對應的 callback function

例如設定 debounce time 是 500ms

洛
洛杉 (不到 500 ms 就出現下一次 input 了,所以不會觸發事件)
洛杉磯(不到 500 ms 就出現下一次 input 了,所以不會觸發事件)
洛杉磯湖 (不到 500 ms 就出現下一次 input 了,所以不會觸發事件)
洛杉磯湖人(過了 500 ms 仍然沒有下個字出現,就發出請求吧!)


(註:並不是所有輸入匡都要實作 debounce,還是要根據自己的需求決定,有興趣的人可以到 google 看看,每一次 input 都是會發出 network request 的)

所以說 Debounce 得概念如果用動畫來看的話會是這個樣子

是不是比較好理解了呢?

Throttle

Throttle 也就是節流的意思,它是另一種減緩事件觸發的方法,概念為使用者觸發相同事件時提供間隔,控制特定時間內事件觸發的次數。Throttle 意味著確保某個函數在指定時間段內最多被調用一次(例如,每 10 秒一次),並確保功能以固定速率定期運行。

同樣看起來十分抽象,所以也試著舉一個生活化的例子來解釋吧!
大家應該都玩過格鬥類的電玩遊戲吧,例如說我這個年紀的人童年的回憶--小朋友齊打交 LF2

通常是按一個按鍵就可以讓操縱的人物進行攻擊,而每一個人物的攻擊速度會依照能力值而有所不同,例如冰人可能每隔三秒才能再吹出下一個龍捲風

但我們都知道在玩遊戲的時候大家的心情都是激動的,在激戰的時候往往會狂按鍵盤,希望角色的攻擊速度可以快一點 ?,這時候為了讓角色依照原本的設定,每隔幾秒才能用一次絕招,必須對使用者的 input 事件做 throttle,避免使用者可以一直不停的丟絕招。

Throttle 在前端的應用可能像是你想在使用者捲動畫面時做些對應的操作,但是有時候使用者可能會一直連續的滾動畫面,例如在 Infinite Scroll 的場景,這會導致 callback 被呼叫太頻繁,有可能導致效能的瓶頸,這時就可以使用 Throttle 技巧限制一段時間內該操作只能被呼叫幾次。

如何實作 Debounce & Throttle

其實像 Lodash 這種函式庫就收錄了 debounce 與 throttle 的方法可以直接拿來用,或是專門用來處理分同步串流的 RxJS 也有提供 debounce 與 throttle 的 function。

而如果要用 JavaScript 自己實作這兩個 function 也是可以的

// Debounce

function customDebounce(func, delay) {
  let timer = null;
 
  return () => {
    const context = this;
    const args = arguments;
 
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  }
}

function handleScroll() {
  console.log('Do Something...');
}

window.addEventListener('scroll', customDebounce(handleScroll, 300));
// Throttle

const customThrottle = (func, limit) => {
  let lastFunc;
  let lastRan;
  return function() {
    const context = this;
    const args = arguments;
    if (!lastRan) {
      func.apply(context, args)
      lastRan = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(function() {
          if ((Date.now() - lastRan) >= limit) {
            func.apply(context, args);
            lastRan = Date.now();
          }
       }, limit - (Date.now() - lastRan));
    }
  }
}
function handleScroll() {
  console.log('So Something')
}

window.addEventListener('scroll', customThrottle(handleScroll, 500));  // 每 500ms 才能再執行

Debounce 的實作還蠻單純的,在函數域加入一個計時器,如果事件一直觸發,便刷新計時器,直至計時器時限內沒有觸發該事件,便執行該事件。

Throttle 就稍微複雜了一些,實現方法是在函數域加入一個計時器並記錄最新一次執行的時間點,並在後續又有事件發生時把現在的時間點與記錄的時間點作比較,如果兩者差距超過設定時限,便允許再次執行該事件的 handler,並設立新的執行時間點。

懂的善用 Debounce 與 Throttle 之後就可以減少一些不必要的操作或是請求囉,不僅能提升前端應用的效能,也發揮了節流器的功能,為接收請求的 server 減緩了一些負擔。

本日小結

如果認真要說前端在面對高流量時能夠幫上什麼忙,我認為其實能做的有限,像是剛剛提到的 Cache 機制,也只能針對 GET 請求,如果是寫入資料或是更新資料的操作就沒辦法被快取了,更不用說讀取資料相對於寫入或修改資料已經更加簡單迅速了。所以說我認為一個系統要能夠處理大流量,本質是還是得從伺服器端或系統面來著手。但這不代表前端幫不上任何忙,我們還是可以盡量為伺服器減輕一些負擔,也就是當伺服器的節流器,盡量減少對伺服器發出請求與降低請求的大小,累積起來也能夠為伺服器減輕不少負擔。

讓我覺得更有趣的一點是今天講的節流方式除了 Debounce 與 Throttle 之外其實都在系列文前面的篇章都介紹過了,所以今天有點算是個小整理,看看之前學的優化技巧可以達到什麼效果,不知道讀者這 29 天是不是都有吸收進去呢?

扣掉第一天的大綱與明天最後一天的完賽心得,基本上這個系列文跟技術相關的篇章就到這裡結束了,真心希望可以讓讀者有所收穫!感性的話留著明天說好了,雖然明天不會講到什麼技術層面的內容,但還是想有始有終的總結一下這 30 天的旅程,希望一直支持到現在的朋友明天也可以造訪一下,我們明天見囉~終於要完賽啦!

Reference & 圖片來源

https://mropengate.blogspot.com/2017/12/dom-debounce-throttle.html

https://towardsdev.com/debouncing-and-throttling-in-javascript-8862efe2b563

https://codesandbox.io/s/1r029moq9l

https://www.pcmarket.com.hk/little-fighter-2-10000-players-nft-project/


上一篇
Day28 X Runtime Performance Debugging
下一篇
Day30 X 系列文總結
系列文
今晚,我想來點 Web 前端效能優化大補帖!30

尚未有邦友留言

立即登入留言