百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

Map 会比 Lodash 更快吗?JS 数组性能优化终极跑分

wptr33 2025-03-14 21:29 28 浏览

观前须知

  • 本文的目的绝非压榨代码性能,本文提供通俗易懂的方法,而不需要深度学习数据结构和算法。
  • 具备 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.06370800733566284

10

0.00720900297164917

100

0.24524998664855957

1_000

20.85587501525879

10_000

2028.1058329939842

100_000

202138.53395798802

1_000_000

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

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

Lodash(原始值)

代码示例

_.uniq(list)

基准测试

时间(毫秒)

数组元素

0.04329100251197815

10

0.09937500953674316

100

0.060499995946884155

1_000

0.49754098057746887

10_000

4.50279101729393

100_000

46.793334007263184

1_000_000

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

Lodash(非原始值)

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

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

代码示例

_.uniqBy(list, comparator)

基准测试

时间(毫秒)

数组元素

0.13112500309944153

10

0.07079198956489563

100

0.42158299684524536

1_000

5.113041996955872

10_000

12.49974998831749

100_000

73.71970799565315

1_000_000

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

Set(原始值)

代码示例

;[...new Set(list)]

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

基准测试

时间(毫秒)

数组元素

0.008958995342254639

10

0.005667001008987427

100

0.0382080078125

1_000

0.36887499690055847

10_000

3.9749999940395355

100_000

43.52562499046326

1_000_000

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

Map(原始值)

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

代码示例

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

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

基准测试

时间(毫秒)

数组元素

0.026500016450881958

10

0.014999985694885254

100

0.12958300113677979

1_000

1.3451250195503235

10_000

8.251917004585266

100_000

158.00600001215935

1_000_000

测评跑分比 Lodash 慢了 2 倍。

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

双列表比较

原生数组方法

代码示例

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

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

基准测试

时间(毫秒)

数组元素

0.01491701602935791

1

0.005333006381988525

10

0.04645800590515137

100

3.2547500133514404

1_000

313.62366700172424

10_000

31434.29237499833

100_000

3210745.023000002

1_000_000

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

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

Lodash(原始值)

代码示例

_.difference(arr1, arr2)

基准测试

时间(毫秒)

数组元素

0.12604200839996338

1

0.09495800733566284

10

0.26454201340675354

100

1.7619580030441284

1_000

11.456708997488022

10_000

30.76341700553894

100_000

376.1795829832554

1_000_000

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

Lodash(非原始值)

代码示例

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

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

_.differenceBy(arr1, arr2, comparator)

基准测试

时间(毫秒)

数组元素

0.24208301305770874

1

0.1150830090045929

10

1.638416975736618

100

1.484584003686905

1_000

15.348375022411346

10_000

35.60387501120567

100_000

590.6338749825954

1_000_000

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

Set(原始值)

代码示例

const arr2Set = new Set(arr2)

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

基准测试

时间(毫秒)

数组元素

0.02225002646446228

1

0.008125007152557373

10

0.032958000898361206

100

0.30558401346206665

1_000

3.6421670019626617

10_000

43.25270900130272

100_000

737.2637079954147

1_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.04791700839996338

1

0.02158302068710327

10

0.0885000228881836

100

0.517208993434906

1_000

4.826333999633789

10_000

88.70929199457169

100_000

1597.0950419902802

1_000_000

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

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

按属性合并列表

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

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

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

原生 JS 方法(map/find)

代码示例

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

基准测试

时间(毫秒)

数组元素

0.021625012159347534

1

0.011750012636184692

10

0.13941702246665955

100

5.005832999944687

1_000

208.6930420100689

10_000

20707.64387497306

100_000

2087215.1352920234

1_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.03525000810623169

1

0.030667006969451904

10

0.15033301711082458

100

1.9047499895095825

1_000

7.687875002622604

10_000

84.34062498807907

100_000

960.1207909882069

1_000_000

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

Lodash

代码示例

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

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

基准测试

时间(毫秒)

数组元素

0.4717079997062683

1

0.24620798230171204

10

0.34333401918411255

100

2.9508340060710907

1_000

17.965292006731033

10_000

194.1733749806881

100_000

6806.113000005484

1_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.02512499690055847

1

0.016208022832870483

10

0.027875006198883057

100

0.23816600441932678

1_000

2.2608749866485596

10_000

24.74924999475479

100_000

576.2636669874191

1_000_000

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

高能总结

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

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

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

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

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

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

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

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

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

Map/Set 都棒棒哒!

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

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

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

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

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

相关推荐

什么是Java中的继承?如何实现继承?

什么是继承?...

Java 继承与多态:从基础到实战的深度解析

在面向对象编程(OOP)的三大支柱中,继承与多态是构建灵活、可复用代码的核心。无论是日常开发还是框架设计,这两个概念都扮演着至关重要的角色。本文将从基础概念出发,结合实例与图解,带你彻底搞懂Java...

Java基础教程:Java继承概述_java的继承

继承概述假如我们要定义如下类:学生类,老师类和工人类,分析如下。学生类属性:姓名,年龄行为:吃饭,睡觉老师类属性:姓名,年龄,薪水行为:吃饭,睡觉,教书班主任属性:姓名,年龄,薪水行为:吃饭,睡觉,管...

java4个技巧:从继承和覆盖,到最终的类和方法

日复一日,我们编写的大多数Java只使用了该语言全套功能的一小部分。我们实例化的每个流以及我们在实例变量前面加上的每个@Autowired注解都足以完成我们的大部分目标。然而,有些时候,我们必须求助于...

java:举例说明继承的概念_java继承的理解

在现实生活中,继承一般指的是子女继承父辈的财产。在程序中,继承描述的是事物之间的所属关系,通过继承可以使多种事物之间形成一种关系体系。例如猫和狗都属于动物,程序中便可以描述为猫和狗继承自动物,同理,...

从零开始构建一款开源的 Vibe Coding 产品 Week1Day4:业界调研之 Agent 横向对比

前情回顾前面两天我们重点调研了了一下Cursor的原理和Cursor中一个关键的工具edit_file的实现,但是其他CodingAgent也需要稍微摸一下底,看看有没有优秀之处,下...

学会这几个插件,让你的Notepad++使用起来更丝滑

搞程序开发的小伙伴相信对Notepad++都不会陌生,是一个占用空间少、打开启动快的文件编辑器,很多程序员喜欢使用Notepad++进行纯文本编辑或者脚本开发,但是Notepad++的功能绝不止于此,...

将 node_modules 目录放入 Git 仓库的优点

推荐一篇文章Whyyoushouldcheck-inyournodedependencies[1]...

再度加码AI编程,腾讯发布AI CLI并宣布CodeBuddy IDE开启公测

“再熬一年,90%的程序员可能再也用不着写for循环。”凌晨两点半,王工还在公司敲键盘。他手里那份需求文档写了足足六页,产品经理反复改了三次。放在过去,光数据库建表、接口对接、单元测试就得写两三天。现...

git 如何查看stash的内容_git查看ssh key

1.查看Stash列表首先,使用gitstashlist查看所有已保存的stash:...

6万星+ Git命令懒人必备!lazygit 终端UI神器,效率翻倍超顺手!

项目概览lazygit是一个基于终端的Git命令可视化工具,通过简易的TUI(文本用户界面)提升Git操作效率。开发者无需记忆复杂命令,即可完成分支管理、提交、合并等操作。...

《Gemini CLI 实战系列》(一)Gemini CLI 入门:AI 上命令行的第一步

谷歌的Gemini模型最近热度很高,而它的...

deepin IDE新版发布:支持玲珑构建、增强AI智能化

IT之家8月7日消息,深度操作系统官方公众号昨日(8月6日)发布博文,更新推出新版deepin集成开发环境(IDE),重点支持玲珑构建。支持玲珑构建deepinIDE在本次重磅更...

狂揽82.7k的star,这款开源可视化神器,轻松创建流程图和图表

再不用Mermaid,你的技术文档可能已经在悄悄“腐烂”——图表版本对不上、同事改完没同步、评审会上被一句“这图哪来的”问得哑口无言。这不是危言耸听。GitHub2025年开发者报告显示,63%的新仓...

《Gemini CLI 实战系列》(五)打造专属命令行工具箱

在前几篇文章中,我们介绍了GeminiCLI的基础用法、效率提升、文件处理和与外部工具结合。今天我们进入第五篇...