Map 会比 Lodash 更快吗?JS 数组性能优化终极跑分
myzbx 2025-07-01 22:12 6 浏览
观前须知
- 本文的目的绝非压榨代码性能,本文提供通俗易懂的方法,而不需要深度学习数据结构和算法。
- 具备 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 毫秒。
相关推荐
- C语言速成之数组:C语言数据处理的核心武器,你真的玩透了吗?
-
程序员Feri一名12年+的程序员,做过开发带过团队创过业,擅长Java、鸿蒙、嵌入式、人工智能等开发,专注于程序员成长的那点儿事,希望在成长的路上有你相伴!君志所向,一往无前!数组:C语言数据处理...
- ES6史上最全数JS数组方法合集-02-数组操作
-
数组生成array.ofletres=Array.of(1,2,3)console.log(res)//[1,2,3]下标定位indexOf用于查找数组中是否存在某个值,如果存...
- 前端性能拉胯?这 8 个 JavaScript 技巧让你的代码飞起来!
-
在前端开发的江湖里,JavaScript就是我们手中的“绝世宝剑”。但为啥别人用剑就能轻松斩敌,你的代码却总拖后腿,页面加载慢、交互卡顿?别着急!今天带来8个超实用的JavaScript实...
- 12种JavaScript中最常用的数组操作整理汇总
-
数组是最常见的数据结构之一,我们需要绝对自信地使用它。在这里,我将列出JavaScript中最重要的几个数组常用操作片段,包括数组长度、替换元素、去重以及许多其他内容。1、数组长度大多数人都知道可...
- 手把手教你在Webpack写一个Loader
-
前言有的时候,你可能在从零搭建Webpack项目很熟悉,配置过各种loader,面试官在Webpack方面问你,是否自己实现过一个loader?如果没有去了解过如果去实现,确实有点尴尬,其...
- const关键字到底该什么用?(可以用const关键字定义变量吗)
-
文|守望先生经授权转载自公众号编程珠玑(id:shouwangxiansheng)前言我们都知道使用const关键字限定一个变量为只读,但它是真正意义上的只读吗?实际中又该如何使用const关键字...
- “JavaScript变量声明三兄弟,你真的会用吗?
-
在JavaScript中,var、let和const是声明变量的关键字,它们在作用域、变量提升、重复声明和重新赋值等方面有显著区别。以下是它们的相同点和不同点,并通过代码示例详细说明。一、相同点声明变...
- ES6(二)let 和 const(es6 var let const区别)
-
let命令let和var差不多,只是限制了有效范围。先定义后使用不管是什么编程语言,不管语法是否允许,都要秉承先定义,然后再使用的习惯,这样不会出幺蛾子。以前JavaScript比较随意,...
- js 里面 let 和 const的区别(js中的let)
-
在JavaScript(包括Vue、Node.js、前端脚本等)中,const和let是用于声明变量的两种方式,它们的主要区别如下:constvslet的区别特性constlet是否...
- JDK21新特性:Sequenced Collections
-
SequencedCollectionsJDK21在JEP431提出了有序集合(SequencedCollections)。引入新的接口来表示有序集合。这样的集合都有一个明确的第一个元素、第二个...
- 动态编程基础——第 2 部分(动态编程是什么)
-
有两种方法可以使用动态规划来解决问题。在这篇文章中,我们将了解制表法。请参阅我的动态编程基础——第1部分了解记忆方法。记忆制表什么是动态规划?它是一种简单递归的优化技术。它大大减少了解决给定...
- Lambda 函数,你真的的了解吗(lambda函数用法)
-
什么是lambda函数lambda函数是一个匿名函数,这意味着与其他函数不同,它们没有名称。这是一个函数,它添加两个数字,写成一个命名函数,可以按其名称调用它们:defadd(x,y):...
- JavaScript 数组操作方法大全(js数组操作的常用方法有哪些)
-
数组操作是JavaScript中非常重要也非常常用的技巧。本文整理了常用的数组操作方法(包括ES6的map、forEach、every、some、filter、find、from、of等)...
- 系列专栏(六):解构赋值(解构赋值默认值)
-
ES6作为新一代JavaScript标准,已正式与广大前端开发者见面。为了让大家对ES6的诸多新特性有更深入的了解,MozillaWeb开发者博客推出了《ES6InDepth》系列文章。CSDN...
- js列表遍历方法解读(js遍历链表)
-
JavaScript提供了多种遍历数组(或列表)的方法。以下是一些常用的方法及其解读:for循环:vararray=[1,2,3,4,5];for(vari=0;...
- 一周热门
- 最近发表
-
- C语言速成之数组:C语言数据处理的核心武器,你真的玩透了吗?
- ES6史上最全数JS数组方法合集-02-数组操作
- 前端性能拉胯?这 8 个 JavaScript 技巧让你的代码飞起来!
- 12种JavaScript中最常用的数组操作整理汇总
- 手把手教你在Webpack写一个Loader
- const关键字到底该什么用?(可以用const关键字定义变量吗)
- “JavaScript变量声明三兄弟,你真的会用吗?
- ES6(二)let 和 const(es6 var let const区别)
- js 里面 let 和 const的区别(js中的let)
- JDK21新特性:Sequenced Collections
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 选择器 (30)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)