我在 medium 上看到一篇 3 JavaScript Performance Mistakes You Should Stop Doing 文章(点击阅读全文可以查看原文,需要访问外国网站),大概意思就是说有 3 个 JavaScript 性能错误,你不应该再去写了。很多“歪果仁”也是一看到这个标题就开始“喷”作者了,下文会详细说。我先介绍下这篇文章的主要内容
当 ES5 发布的时候,JavaScript 引入了很多新的数组函数。其中包括 forEach,reduce,map,filter - 它们让我们感觉语言在不断增长,功能越来越强大,编写代码变得更加有趣和流畅,结果更易于阅读和理解。
大约在同一时间,一个新的环境--Node.js,它使我们能够从前端到后端平稳过渡,同时真正重新定义完整的全栈开发。
所以作者就测试了一下新提供的这些方法是否会影响我们程序的性能。他在 macOS 上对Node.js v10.11.0 和 Chrome 浏览器执行了以下测试。
1. 循环数组
他想到的一个很常见的场景,就是计算一下 10k 项的总和。然后比较了使用 for,for of,while,forEach 和 reduce 的随机 10k 项的总和。运行测试 10,000 次返回以下结果:
For Loop, average loop time: ~10 microseconds
For-Of, average loop time: ~110 microseconds
ForEach, average loop time: ~77 microseconds
While, average loop time: ~11 microseconds
Reduce, average loop time: ~113 microseconds
在谷歌搜索如何对数组求和时,reduce 是最好的解决方案,但它是最慢的。即使是最新的(ES6)也提供了较差的性能。事实证明,老的 for 循环提供了迄今为止最好的性能 - 超过 10 倍以上!
最新推荐的解决方案如何使 JavaScript 变得如此之慢?造成这种痛苦的原因有两个主要原因:reduce 和 forEach 需要执行一个回调函数,这个函数被递归调用并使堆栈膨胀,以及对执行代码进行的附加操作和验证(在此描述 https://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.21)。
2. 复制数组
虽然这听起来不那么有趣,但这是不可变函数的支柱,它在生成输出时不会修改输入。
此处的性能测试结果再次显示了同样有趣的趋势 - 当重复 10k 随机项的 10k 数组时,使用旧的传统解决方案更快。同样最新的 ES6 扩展操作符 [... arr]
和来自 Array.from(arr)
的数组加上 ES5 的 map arr.map(x => x)
不如老的 slice arr.slice()
和 concat [] .concat(arr)
。
Duplicate using Slice, average: ~367 microseconds
Duplicate using Map, average: ~469 microseconds
Duplicate using Spread, average: ~512 microseconds
Duplicate using Conct, average: ~366 microseconds
Duplicate using Array From, average: ~1,436 microseconds
Duplicate manually, average: ~412 microseconds
3. 迭代对象
另一种常见的情况是迭代对象,当我们尝试遍历 JSON 和对象时,这是必要的,而不是寻找特定的键值。同样有老 的解决方案,如 for-in for(let key in obj)
,或者后来的 Object.keys(obj)
(在 es6 中显示)和 Object.entries(obj)
(来自ES8)它返回 keys 和 vaues。
使用上述方法对 10k 个对象迭代进行性能分析,每个迭代包含 1,000 个随机 key 和value,得到以下结论。
Object iterate For-In, average: ~240 microseconds
Object iterate Keys For Each, average: ~294 microseconds
Object iterate Entries For-Of, average: ~535 microseconds
原因是在后两个解决方案中创建了可枚举值数组,而不是在没有 keys 数组的情况下直接遍历对象。但最终结果仍然引起关注。
最后 我的结论很清楚 - 如果快速性能对您的应用程序至关重要,或者您的服务器需要处理一些负载 - 使用最酷,更易读,更感觉的解决方案将会对您的应用程序性能产生重大影响 - 最多可以达到慢 10 倍!
下一次,在盲目采用最新趋势之前,确保它们也符合您的要求 - 对于小型应用程序,快速编写和更易读的代码是完美的 - 但对于压力大的服务器和大型客户端应用程序,这可能不是最好的做法。
评论 1
一个英文名叫 Christian Gitter 评论说:
不同意。谁在乎微秒?
如果您正在开发一个高性能的超级关键服务器应用程序,那么您要么首先不使用 JavaScript,要么您将成为一名经验丰富的开发人员,他知道自己在做什么以及谁不仅仅取得他的第一个结果。 “如何将数组相加”,Google 搜索结果并将其作为目标。
我们假设你有一个你注意到的服务很慢。你有两个选择。选项 1 占用了团队中的一个或几个开发人员,让他们花一些时间来优化代码以提高速度。选项 2 正在投入一些资金来扩展您的硬件。我说几乎总是选择 2。
在短期内,让您的开发人员进行优化工作可能比扩展服务器所需的成本更高。长期成本甚至更高,因为您将不得不继续进行这种优化,并且您将失去代码可读性,因此新开发人员需要更长时间来确定代码的作用。
这是你几乎应该做的事情:
[].forEach(…)
const newArray = [… oldArray]
Object.values(obj).forEach()
…或者如果你想要 keys => Object.entries(obj).forEach()
…或者只是 keys => Object.keys(obj).forEach()
我想你明白我的意思。
作者回复:
“谁在乎微秒?” - 好吧,在我工作的地方,我们每天处理大约550亿个事件,这意味着每秒大约700k个事件,当我们尝试在这种环境中运行节点时……你知道其余的事情。
Stephen Young 回复作者
好吧,让我们对每秒 700k 事件进行数学计算。让我们说,为了论证,20% 的事件(每秒 140k)正在进行一些繁重的工作并循环超过一万件事情。现在,假设您将这些循环从 forEach
优化到 for
循环。您的“基准”可为此更改节省 67 微秒。700k * 0.20 * 67 等于 938 万微秒。这归结为节省了惊人的 9.38 秒。这些秒不是线性的,因为我假设您没有在单个 JavaScript 线程上使用单个服务器消耗 700k 事件。在那种规模上,你并行运行多个线程。所以在这个非常慷慨的例子中,你每秒循环 10k 项、 140k次……你最终可能节省不到一秒钟。
评论 2
对于真正的应用程序性能,整个博客文章遗憾地是非常糟糕的建议。
在优化性能时应该做的第一件事是找到应用程序的实际瓶颈。否则,花费时间来优化对实际执行时间没有实际影响的代码。我是一名软件架构师,我最喜欢的一件事就是让代码快速发展。根据我的经验,主要的瓶颈主要是算法复杂性差。除此之外,算法中经常出现错误,并且在实现中存在许多奇怪之处。所以请使用 https://clinicjs.org/ 等工具。这有助于找到应该优化的代码。
这篇文章中提到的优化是微优化,降低了代码的可读性,因此代码需要更多的时间来阅读和理解,这导致优化热代码路径的时间更短。性能也只是当前版本的快照,并且由于新的引擎优化,相同的代码在下一版本中可能表现得非常不同。
如果内置函数确实比不同的实现慢得多(由于 V8 团队很厉害,这种情况不再那么常见),请向 V8 团队报告,以便他们可以进一步优化这些部分。还要注意,由于底层引擎优化(死代码消除等),基准测试本身可能不会按预期运行。
首先发布个人看法,我觉得这篇文章还是很有价值的,很有趣的,它带给我们的价值不是说这些比较的数字比较有价值,而是另外的两点:
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有