Web 性能優化:理解及使用 JavaScript 緩存

阿里雲最近在作活動,低至2折,有興趣能夠看看:
https://promotion.aliyun.com/...

爲了保證的可讀性,本文采用意譯而非直譯。html

這是 Web 性能優化的第 5 篇,上一篇在下面看點擊查看:前端

  1. Web 性能優化:使用 Webpack 分離數據的正確方法
  2. Web 性能優化:圖片優化讓網站大小減小 62%
  3. Web 性能優化:緩存 React 事件來提升性能
  4. Web 性能優化:21種優化CSS和加快網站速度的方法

隨着咱們的應用程序的不斷增加並開始進行復雜的計算時,對速度的需求愈來愈高(🏎️),因此流程的優化變得必不可少。 當咱們忽略這個問題時,咱們最終的程序須要花費大量時間並在執行期間消耗大量的系統資源。git

緩存是一種優化技術,經過存儲開銷大的函數執行的結果,並在相同的輸入再次出現時返回已緩存的結果,從而加快應用程序的速度。程序員

若是這對你沒有多大意義,那不要緊。 本文深刻解釋了爲何須要進行緩存,緩存是什麼,如何實現以及什麼時候應該使用緩存。github

什麼是緩存

緩存是一種優化技術,經過存儲開銷大的函數執行的結果,並在相同的輸入再次出現時返回已緩存的結果,從而加快應用程序的速度。

在這一點上,咱們很清楚,緩存的目的是減小執行「昂貴的函數調用」所花費的時間和資源。npm

什麼是昂貴的函數調用?別搞混了,咱們不是在這裏花錢。在計算機程序的上下文中,咱們擁有的兩種主要資源是時間和內存。所以,一個昂貴的函數調用是指一個函數調用中,因爲計算量大,在執行過程當中大量佔用了計算機的資源和時間。redux

然而,就像對待金錢同樣,咱們須要節約。爲此,使用緩存來存儲函數調用的結果,以便在未來的時間內快速方便地訪問。segmentfault

緩存只是一個臨時的數據存儲,它保存數據,以便未來對該數據的請求可以更快地獲得處理。

所以,當一個昂貴的函數被調用一次時,結果被存儲在緩存中,這樣,每當在應用程序中再次調用該函數時,結果就會從緩存中很是快速地取出,而不須要從新進行任何計算。緩存

爲何緩存很重要?

下面是一個實例,說明了緩存的重要性:性能優化

想象一下,你正在公園裏讀一本封面很吸引人的新小說。每次一我的通過,他們都會被封面吸引,因此他們會問書名和做者。第一次被問到這個問題的時候,你翻開書,讀出書名和做者的名字。如今愈來愈多的人來這裏問一樣的問題。你是一個很好的人🙂,因此你回答全部問題。

你會翻開封面,把書名和做者的名字一一告訴他,仍是開始憑記憶回答?哪一個能節省你更多的時間?

發現其中的類似之處了嗎?使用記憶法,當函數提供輸入時,它執行所需的計算並在返回值以前將結果存儲到緩存中。若是未來接收到相同的輸入,它就沒必要一遍又一遍地重複,它只須要從緩存(內存)中提供答案。

緩存是怎麼工做的

JavaScript 中的緩存的概念主要創建在兩個概念之上,它們分別是:

  • 閉包
  • 高階函數(返回函數的函數)

閉包

閉包是函數和聲明該函數的詞法環境的組合。

不是很清楚? 我也這麼認爲。

爲了更好的理解,讓咱們快速研究一下 JavaScript 中詞法做用域的概念,詞法做用域只是指程序員在編寫代碼時指定的變量和塊的物理位置。以下代碼:

function foo(a) {
  var b = a + 2;
  function bar(c) {
    console.log(a, b, c);
  }
  bar(b * 2);
}

foo(3); // 3, 5, 10

從這段代碼中,咱們能夠肯定三個做用域:

  • 全局做用域(包含 foo 做爲惟一標識符)
  • foo 做用域,它有標識符 abbar
  • bar 做用域,包含 c 標識符

仔細查看上面的代碼,咱們注意到函數 foo 能夠訪問變量 a 和 b,由於它嵌套在 foo 中。注意,咱們成功地存儲了函數 bar 及其運行環境。所以,咱們說 barfoo 的做用域上有一個閉包。

你能夠在遺傳的背景下理解這一點,即個體有機會得到並表現出遺傳特徵,即便是在他們當前的環境以外,這個邏輯突出了閉包的另外一個因素,引出了咱們的第二個主要概念。

從函數返回函數

經過接受其餘函數做爲參數或返回其餘函數的函數稱爲高階函數。

閉包容許咱們在封閉函數的外部調用內部函數,同時保持對封閉函數的詞法做用域的訪問

讓咱們對前面的示例中的代碼進行一些調整,以解釋這一點。

function foo(){
  var a = 2;

  function bar() {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz();//2

注意函數 foo 如何返回另外一個函數 bar。這裏咱們執行函數 foo 並將返回值賦給baz。可是在本例中,咱們有一個返回函數,所以,baz 如今持有對 foo 中定義的bar 函數的引用。

最有趣的是,當咱們在 foo 的詞法做用域以外執行函數 baz 時,仍然會獲得 a 的值,這怎麼可能呢?😕

請記住,因爲閉包的存在,bar 老是能夠訪問 foo 中的變量(繼承的特性),即便它是在 foo 的做用域以外執行的。

案例研究:斐波那契數列

斐波那契數列是什麼?

斐波那契數列是一組數字,以1 或 0 開頭,後面跟着1,而後根據每一個數字等於前兩個數字之和規則進行。如

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

或者

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

挑戰:編寫一個函數返回斐波那契數列中的 n 元素,其中的序列是:

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …]

知道每一個值都是前兩個值的和,這個問題的遞歸解是:

function fibonacci(n) {
  if (n <= 1) {
    return 1
  }
  return fibonacci(n - 1) + fibonacci(n - 2)
}

確實簡潔準確!可是,有一個問題。請注意,當 n 的值到終止遞歸以前,須要作大量的工做和時間,由於序列中存在對某些值的重複求值。

看看下面的圖表,當咱們試圖計算 fib(5)時,咱們注意到咱們反覆地嘗試在不一樣分支的下標 0,1,2,3 處找到 Fibonacci 數,這就是所謂的冗餘計算,而這正是緩存所要消除的。

圖片描述

function fibonacci(n, memo) {
  memo = memo || {}
  if (memo[n]) {
    return memo[n]
  }
  if (n <= 1) {
    return 1
  }

  return memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)
}

在上面的代碼片斷中,咱們調整函數以接受一個可選參數 memo。咱們使用 memo 對象做爲緩存來存儲斐波那契數列,並將其各自的索引做爲鍵,以便在執行過程當中稍後須要時檢索它們。

memo = memo || {}

在這裏,檢查是否在調用函數時將 memo 做爲參數接收。若是有,則初始化它以供使用;若是沒有,則將其設置爲空對象。

if (memo[n]) {
  return memo[n]
}

接下來,檢查當前鍵 n 是否有緩存值,若是有,則返回其值。

和以前的解同樣,咱們指定了 n 小於等於 1 時的終止遞歸。

最後,咱們遞歸地調用n值較小的函數,同時將緩存值(memo)傳遞給每一個函數,以便在計算期間使用。這確保了在之前計算並緩存值時,咱們不會第二次執行如此昂貴的計算。咱們只是從 memo 中取回值。

注意,咱們在返回緩存以前將最終結果添加到緩存中。

使用 JSPerf 測試性能

可使用些連接來性能測試。在那裏,咱們運行一個測試來評估使用這兩種方法執行fibonacci(20) 所需的時間。結果以下:

圖片描述

哇! ! !這讓人很驚訝,使用緩存的 fibonacci 函數是最快的。然而,這一數字至關驚人。它執行 126,762 ops/sec,這遠遠大於執行 1,751 ops/sec 的純遞歸解決方案,而且比較沒有緩存的遞歸速度大約快 99%。

注:「ops/sec」表示每秒的操做次數,就是一秒鐘內預計要執行的測試次數。

如今咱們已經看到了緩存在函數級別上對應用程序的性能有多大的影響。這是否意味着對於應用程序中的每一個昂貴函數,咱們都必須建立一個修改後的變量來維護內部緩存?

不,回想一下,咱們經過從函數返回函數來了解到,即便在外部執行它們,它們也會致使它們繼承父函數的範圍,這使得能夠將某些特徵和屬性從封閉函數傳遞到返回的函數。

使用函數的方式

在下面的代碼片斷中,咱們建立了一個高階的函數 memoizer。有了這個函數,將可以輕鬆地將緩存應用到任何函數。

function memoizer(fun) {
  let cache = {}
  return function (n) {
    if (cache[n] != undefined) {
      return cache[n]
    } else {
      let result = fun(n)
      cache[n] = result
      return result
    }
  }
}

上面,咱們簡單地建立一個名爲 memoizer 的新函數,它接受將函數 fun 做爲參數進行緩存。在函數中,咱們建立一個緩存對象來存儲函數執行的結果,以便未來使用。

memoizer 函數中,咱們返回一個新函數,根據上面討論的閉包原則,這個函數不管在哪裏執行均可以訪問 cache

在返回的函數中,咱們使用 if..else 語句檢查是否已經有指定鍵(參數) n 的緩存值。若是有,則取出並返回它。若是沒有,咱們使用函數來計算結果,以便緩存。而後,咱們使用適當的鍵 n 將結果添加到緩存中,以便之後能夠從那裏訪問它。最後,咱們返回了計算結果。

很順利!

要將 memoizer 函數應用於最初遞歸的 fibonacci 函數,咱們調用 memoizer 函數,將 fibonacci 函數做爲參數傳遞進去。

const fibonacciMemoFunction = memoizer(fibonacciRecursive)

測試 memoizer 函數

當咱們將 memoizer 函數與上面的例子進行比較時,結果以下:

圖片描述

memoizer 函數以 42,982,762 ops/sec 的速度提供了最快的解決方案,比以前考慮的解決方案速度要快 100%。

關於緩存,咱們已經說明什麼是緩存 、爲何要有緩存和如何實現緩存。如今咱們來看看何時使用緩存。

什麼時候使用緩存

固然,使用緩存效率是級高的,你如今可能想要緩存全部的函數,這可能會變得很是無益。如下幾種狀況下,適合使用緩存:

  • 對於昂貴的函數調用,執行復雜計算的函數。
  • 對於具備有限且高度重複輸入範圍的函數。
  • 用於具備重複輸入值的遞歸函數。
  • 對於純函數,即每次使用特定輸入調用時返回相同輸出的函數。

緩存庫

總結

使用緩存方法 ,咱們能夠防止函數調用函數來反覆計算相同的結果,如今是你把這些知識付諸實踐的時候了。

你的點贊是我持續分享好東西的動力,歡迎點贊!

交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

https://github.com/qq44924588...

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

clipboard.png

相關文章
相關標籤/搜索