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

相关文章
相关标签/搜索