lodash库本身提供了lodash-es导出ES Module格式,可用于webpack的Tree Shaking操作。但是如果一但使用了lodash/fp模块,则必然会出现如下的情况:

可以看到在lodash模块中,lodash.min.js被直接引入了,同时又同时引入了一个lodash.js以及很多小的模块,代码的重复度非常高。
这一问题的起因是lodash的fp模块直接引用了min文件,官方的说法是为了解析提升速度。
与此同时,官方给出了2个方案来解决体积过大的问题:
根据官方的说法,这两者配合起来以后,能有效地降低lodash的体积。因此我们试图做一个实验。
一个简单的项目
首先我们拿了一个相对简单的项目,这是其src部分的大致数据:
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
JavaScript 174 1153 669 6562
LESS 51 286 12 1456
Markdown 1 39 0 84
-------------------------------------------------------------------------------
SUM: 226 1478 681 8102
-------------------------------------------------------------------------------不到万行的小项目,对lodash的使用也比较有限,使用grep简单做了下统计,大概使用了这么些函数:
lodash/fp: get isEmpty isEqual map mapValues memoize omit sortBy
lodash: get intersection isEmpty isNil last noop range我们分别测试了3种配置的构建:
- 不引入任何优化。
- 引入
babel-plugin-lodash和lodash-webpack-plugin,同时为了安全,将lodash-webpack-plugin的配置全部打开。 - 与上一种类似,但
lodash-webpack-plugin的配置为默认,即使用最优效果。
得到的结论:
| 模式 | 原始体积 | gzip后体积 |
| -------- | -------- | ---------- |
| 无优化 | 189.31KB | 62.13KB |
| 安全配置 | 63.96KB | 19.33KB |
| 最优配置 | 40.21KB | 11.86KB |可以看到,在使用相关的优化后,可以得到不小的体积优势。同时从模块组成上,也能清晰地看到lodash.min已经消失了:

在一个有限使用lodash的项目上,我们的实验得到了成功,最佳状态带来了约50KB的gzip体积的优势,但是考虑到最优配置会移除很多特性,如const toAuthorName = map('author.name')这种形式是无法使用的,因此几乎可以确定这个构建出来的代码在运行时一定会出错。
换一个复杂的项目
为了继续测试不同的情况,这一次我们找了一个相对复杂的项目,其情况如下:
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
JSX 482 4411 2368 32424
LESS 135 1842 104 8120
JavaScript 16 99 76 441
-------------------------------------------------------------------------------
SUM: 633 6352 2548 40985
-------------------------------------------------------------------------------4万多出个零头的代码量,虽然还够不成过10万的超大型系统,但也足够分量。继续简单地统计了其对lodash的使用情况:
lodash: camelCase compact constant difference drop each escape fill filter find findIndex findLast findLastIndex flatMap flatten floor forEach forIn get identity isArray isBoolean isEmpty isEqual isFunction isNull isObject isString isUndefined keyBy keys last map mapValues max memoize merge noop omit omitBy partial pick random range reverse some sortBy sum sumBy take transform trimEnd union uniq values zipObject
lodash/fp: compact constant countBy eq filter find findIndex flatMap flow get getOr groupBy has identity inRange isBoolean isEmpty isEqual isNull isNumber isUndefined keyBy last map mapValues maxBy memoize noop omit omitBy over partial pick property range reduce sortBy stubFalse sumBy uniq values zipObject从数量上来看,对lodash的应用是比较多的,且lodash/fp模块的应用比较广泛。我们同样地对其做了3次实验,得到结果如下:
| 模式 | 原始体积 | gzip后体积 |
| -------- | -------- | ---------- |
| 无优化 | 203.18KB | 66.11KB |
| 安全配置 | 203.18KB | 66.12KB |
| 最优配置 | 179.44KB | 58.42KB |奇怪的结果是,优化对这个项目的lodash几乎无效,我们也能看到lodash.min依然存在于最后的包中:

这就让人非常尴尬了……
寻找问题函数
事到如今,总不能二话不说就认为复杂项目无法优化lodash。为了找出真正导致问题的这个函数,我们尝试复现一下问题,写一个简单的文件,将用到的函数全部引入进来并调用一下,再用webpack做一下优化全开的构建来看下结果:
| 原始体积 | gzip后体积 |
| -------- | ---------- |
| 34.45KB | 10.16KB |嗯,奇迹果然不会这么快出现的呢……在四处排查了一圈以后,我们确定体积问题是由于每三方库使用require引入了lodash所致。babel-plugin-lodash仅能处理ES Module形式的引入,对于CommonJS等其它模块格式无法支持。这导致一但有第三方库使用非ES Module的形式整体引入lodash,所有的优化都会付诸东流。
结论
通过一些简单的实验,我们可以得到几个基本结论:
babel-plugin-lodash和lodash-webpack-plugin对使用ES Module引入lodash的场景有不错的优化效果。- 是否使用ES Module引入lodash取决于业务的源代码及所有第三方库的态度,在当前npm生态中依旧是CommonJS占大头,大部分包并没有提供
module字段的前提下,优化的概率有限。比如我们的状态管理库standard-redux-shape也是在最近才缩减了lodash的尺寸。
同时考虑到,在正常的Chunk拆分的思路下,lodash这种使用范围经常变化的库的Tree Shaking可能导致对应的Chunk的hash不稳定,让缓存频繁地失效,因此我们选择放弃了lodash的优化,保持整个lodash引入的状态,以50KB左右体积的代价换取hash的绝对稳定。