likes
comments
collection
share

Map 会比 Lodash 更快吗?JS 数组性能优化终极跑分数组是 JS 中人气爆棚的数据结构之一。 本期共享的数组模

作者站长头像
站长
· 阅读数 31

大家好,这里是大家的林语冰。欢迎持续关注“前端俱乐部”~

数组是 JS 中人气爆棚的数据结构之一。

虽然但是,我很少关注数组更快的性能,因为我编写的大多数代码没有性能的需求。

因此,每当我的代码中出现性能问题时,我都十分鸡冻,因为这意味着,我有机会接触性能优化的问题。

本期共享的数组模式的性能优化,让我们瞄一下 JS 数组模式的常规操作,以及如何使其速度更快。

Map 会比 Lodash 更快吗?JS 数组性能优化终极跑分数组是 JS 中人气爆棚的数据结构之一。 本期共享的数组模

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 How to Optimize Common JavaScript Array Patterns

观前须知

  • 本文的目的绝非压榨代码性能,本文提供通俗易懂的方法,而不需要深度学习数据结构和算法。
  • 具备 Map/Set 的知识储备会有所助益,因为本文的所有示例需要使用它们。
  • 对于所有示例,我们都会测评 3 种不同方案:
    • 原生 JS 数组方法(filter/reduce/map 等)
    • Lodash 工具库
    • Map/Set
  • 所有示例均包含性能基准测试,因为除非我们测评跑分,否则性能优化没有任何统计学意义。
  • 在大多数跑分中,Lodash 比 Map/Set 有过之而无不及。但我仍会表演 Map/Set 的方案,毕竟我们可能不想安装 Lodash 依赖。
  • 我只表演不可变操作,因为我的大部分工作都受益于不可变操作。

元素去重

原生数组方法

代码示例

list.filter((item, pos) => {
  return list.indexOf(item) === pos
})

基准测试

时间(毫秒)数组元素
0.0637080073356628410
0.00720900297164917100
0.245249986648559571_000
20.8558750152587910_000
2028.1058329939842100_000
202138.533957988021_000_000

根据基准测试,此代码在处理 10_000100_000 条记录时性能差强人意,超过该阈值则无法接受。

如果此代码在浏览器运行,那么在此期间我们的网站会卡死大约 3 分钟。

Lodash(原始值)

代码示例

_.uniq(list)

基准测试

时间(毫秒)数组元素
0.0432910025119781510
0.09937500953674316100
0.0604999959468841551_000
0.4975409805774688710_000
4.50279101729393100_000
46.7933340072631841_000_000

夭寿啦!一旦高达 1_000_000 条记录,速度就会快近 4_000 倍!Lodash 绝对是正确的打开方式。

Lodash(非原始值)

上述优化性能惊人,但能且仅能用于原始值(字符串、数字、布尔值等)。

如果我们想基于属性实现元素唯一性,那该怎么办呢?

代码示例

_.uniqBy(list, comparator)

基准测试

时间(毫秒)数组元素
0.1311250030994415310
0.07079198956489563100
0.421582996845245361_000
5.11304199695587210_000
12.49974998831749100_000
73.719707995653151_000_000

虽然性能降低了一点点,但测评跑分仍低于 100 毫秒!

Set(原始值)

代码示例

;[...new Set(list)]

Set 的方案简单粗暴,这能奏效,因为 Set 能且仅能接受唯一值。

基准测试

时间(毫秒)数组元素
0.00895899534225463910
0.005667001008987427100
0.03820800781251_000
0.3688749969005584710_000
3.9749999940395355100_000
43.525624990463261_000_000

测评跑分和 Lodash 平分秋色!现在我们可以把 Lodash 删了吧!

Map(原始值)

如果我们用非原始值测评上述例子,这无法奏效,因为 Set 能且仅能识别原始值的唯一性。此乃 Map 的用武之地!

代码示例

new Map(list.map(item => [extractKey(item), item])).values()

Map 的工作机制与 Set 类似,因为键值必须唯一,虽然但是,它们的键会映射到值!

基准测试

时间(毫秒)数组元素
0.02650001645088195810
0.014999985694885254100
0.129583001136779791_000
1.345125019550323510_000
8.251917004585266100_000
158.006000012159351_000_000

测评跑分比 Lodash 慢了 2 倍。

虽然但是,如果我们想避免非必要的依赖,私以为这种性能也差强人意。

双列表比较

原生数组方法

代码示例

当我百度一下“JS 中的数组比较”时,StackOverflow 上爆料的首个答案是:

let difference = arr1.filter(x => !arr2.includes(x))

基准测试

时间(毫秒)数组元素
0.014917016029357911
0.00533300638198852510
0.04645800590515137100
3.25475001335144041_000
313.6236670017242410_000
31434.29237499833100_000
3210745.0230000021_000_000

测评跑分完全达咩。100_000 条记录一共需要 30 秒,速度慢如龟速。

但一旦达到 1_000_000,就耗时将近一小时。让我们瞄一下其他方案能否成功优化。

Lodash(原始值)

代码示例

_.difference(arr1, arr2)

基准测试

时间(毫秒)数组元素
0.126042008399963381
0.0949580073356628410
0.26454201340675354100
1.76195800304412841_000
11.45670899748802210_000
30.76341700553894100_000
376.17958298325541_000_000

舒服了。即使有 1_000_000 条记录,我们连一秒钟都不需要!

Lodash(非原始值)

代码示例

举一反一,如果我们在非原始值的情况下测评跑分,它不再奏效。

幸运的是,Lodash 提供了解决方案。

_.differenceBy(arr1, arr2, comparator)

基准测试

时间(毫秒)数组元素
0.242083013057708741
0.115083009004592910
1.638416975736618100
1.4845840036869051_000
15.34837502241134610_000
35.60387501120567100_000
590.63387498259541_000_000

测评跑分慢了 200 毫秒,但性能仍对原生数组方法“降维打击”。

Set(原始值)

代码示例

const arr2Set = new Set(arr2)

arr1.filter(x => !arr2Set.has(x))

基准测试

时间(毫秒)数组元素
0.022250026464462281
0.00812500715255737310
0.032958000898361206100
0.305584013462066651_000
3.642167001962661710_000
43.25270900130272100_000
737.26370799541471_000_000

举一反一,测评跑分比 Lodash 慢,但比原生数组方法快。

此方案比使用原生数组方法更快,是因为 Set.has 能奏效。Set 在存值时会计算其哈希值,并将该值存储在该键下。

这使得读写一个值需要 O(1) 时间复杂度,而 Array.includes 需要 O(n) 时间复杂度。

简直酷毙了,对不?

Map(非原始值)

代码示例

const arr2Set = new Map(arr2.map(x => [extractKey(x), x]))

arr1.filter(x => !arr2Set.has(extractKey(x)))

基准测试

时间(毫秒)数组元素
0.047917008399963381
0.0215830206871032710
0.0885000228881836100
0.5172089934349061_000
4.82633399963378910_000
88.70929199457169100_000
1597.09504199028021_000_000

这是第一个突破 1 秒标记的优化。

测评跑分仍比原生方法更快,但 2 倍的速度提升可能使得在项目中导入 Lodash 变得物有所值。

按属性合并列表

此操作采用 2 个具有共同属性的列表,并返回包含这些匹配对象的对象列表。

粉丝请注意:对于此操作,我们基于以下假设:

  • 两个列表长度相同。
  • 任一列表中都具有重复属性的元素。
  • 每个列表中的每个元素在另一个列表中都有对应的元素。

原生 JS 方法(map/find

代码示例

listB.map(b => ({
  b: b,
  a: listA.find(a => a[aProperty] === b[bProperty])
}))

基准测试

时间(毫秒)数组元素
0.0216250121593475341
0.01175001263618469210
0.13941702246665955100
5.0058329999446871_000
208.693042010068910_000
20707.64387497306100_000
2087215.13529202341_000_000

梅开二度,使用 JS 数组方法又变慢了。

原生 JS 方法(reduce/map

在为此操作的 Map/Set 版本进行基准测试时,我发现了另一种更高效的方案,来使用原生数组方法执行此操作。

代码示例

const listAMapById = listA.reduce((acc, a) => {
  return Object.assign(acc, { [a[aProperty]]: a })
}, {})

listB.map(b => ({
  b: b,
  a: listAMapById[b[bProperty]]
}))

在此示例中,我们将其中一个列表处理为一个对象,然后在查找另一个列表的对象时索引到该列表。

基准测试

时间(毫秒)数组元素
0.035250008106231691
0.03066700696945190410
0.15033301711082458100
1.90474998950958251_000
7.68787500262260410_000
84.34062498807907100_000
960.12079098820691_000_000

夭寿啦!使用原生 JS 数组方法,这一次并没有慢得令人窒息!

Lodash

代码示例

_.mergeWith(_.sortBy(listA, aProperty), _.sortBy(listB, bProperty), (a, b) => ({
  a,
  b
}))

我无法找到 Lodash 提供的开箱即用的方法,但我有一个大胆的想法。如果不满足上述任何假设,那么该方法也爱莫能助。

基准测试

时间(毫秒)数组元素
0.47170799970626831
0.2462079823017120410
0.34333401918411255100
2.95083400607109071_000
17.96529200673103310_000
194.1733749806881100_000
6806.1130000054841_000_000

令人喵瞪狗呆的是,使用 Lodash 并不能吊打原生 JS 数组的性能!

这可能因为,在实际将两个数组合并之前,需要对它们排序造成的。

Map

代码示例

const listAMapByProperty = new Map(listA.map(a => [a[aProperty], a]))

listB.map(b => ({
  b,
  a: listAMapByProperty.get(b[bProperty])
}))

基准测试

时间(毫秒)数组元素
0.025124996900558471
0.01620802283287048310
0.027875006198883057100
0.238166004419326781_000
2.260874986648559610_000
24.74924999475479100_000
576.26366698741911_000_000

这次原生方法可能击败了 Lodash,但在此情况下,使用 Map 似乎是其中最快的。

高能总结

这些是我在这些基准测试中收获的东东。

使用 Lodash 是最快的(大多数情况下)

运行这些基准测试后,我阅读了我使用的 Lodash 方法的源码。

大多数情况下,Lodash 使用 MapSet 来获得这种性能。

虽然但是,Lodash 也进行了为微调,挤出了额外的性能优势。

因此,如果性能对您而言兹事体大,且您不介意导入 npm 包,那么如果您正在处理包含海量元素的数据,您可以优先使用 Lodash。

然而情况并非总是如此,因此粉丝请务必深度学习多种方案,运行基准测试。

您不需要 Lodash 来获得优秀的性能

虽然 Lodash 是最快的,但如果没有 Lodash,我们也有其他无限逼近其速度的技术方案。

Map/Set 都棒棒哒!

运行所有基准测试后,我肯定会开始在代码中更多地使用 Set/Map

它们不仅速度惊人,而且有手就行,并提供了良好的 API 来操作。

JS 数组方法对于少量数据而言足够快。

如果运行的数组的元素数量不超过 10_000,那可能不需要过早的性能优化。

我进行基准测试的所有操作,在该体量的数据集上执行的时间都超过 300 毫秒。

本期话题是:你经常在项目中使用 Lodash 吗,或者你有其他数组性能优化的奇技淫巧吗?

欢迎在本文下方自由言论,文明共享。

欢迎持续关注“前端俱乐部”!坚持阅读,自律打卡,每天一次,进步一点。

谢谢大家的点赞,掰掰~

Map 会比 Lodash 更快吗?JS 数组性能优化终极跑分数组是 JS 中人气爆棚的数据结构之一。 本期共享的数组模

转载自:https://juejin.cn/post/7360890020121804836
评论
请登录