Why you should (still) embrace lodash

As there are already a number of articles suggesting to abolish lodash and especially after having read and seeing this codeburst article, I feel the need for some correction and differentiation in favor for lodash.

For the discussion on whether to use or not use lodash, I put its functionality into three groups.

  • Functionality not available natively (like _.keyBy(), _.throttle(), _.memoize() and many more)
    Lodash consists of many functions that are not available natively. They are most likely better than then any version you would write. It is hard to make an argument against them, so I strongly suggest to know and use them.
  • Functionality also available natively (like _,map(), _.filter(), _.reduce())
    Articles suggesting to abolish lodash focus on those functions and ignore the far greater number of functionality without native alternative. Usefulness of natively available lodash counterparts is indeed debatable. This article will discuss them further below. My thesis is, you usually should use them.
  • Functionality already available in ES6 (like _.assign(), _.head(), _.tail(), _.clone())
    As long as you are not living in a legacy environment without babel or have to work with developers not willing to learn ES6, there is indeed not much reason to use them. On the other hand, the existence of a pure ES6 solution does not mean that you are not allowed to use lodash’s variants.

Why you should embrace lodash

You are welcome to skip this section if this is a no-brainer for you.

Increased productivity and readability

Lodash provides a big amount of utility functions which you do not know how much you need them without knowing them. Helpers like _.memoize() and _.throttle() provide key functionality for quality front-ends. Other data conversion functions operating on arrays, objects and collections allow you to stop worrying about anything else than your algorithm.

You might write this:

or you can reach the same by using an equivalent lodash function:

Guess what is faster to write? Guess what is faster to read? The answer is obvious. Code is much more often read and (hopefully) understood than written. So using the lodash functionality is not only a productivity gain when you write it, it can save time every time someone looks in that code. This is of high importance for the quality of your codebase.

By using lodash you actually write much less code and in the same time improve readability, quality and stability. It can provide a huge boost in productivity for you and your team.

Additionally you can assume that lodash’s version is more optimized than the version you would write. The productivity gains adds up because you usually have tons of data such transformations in a project.

This is not just about code becoming smaller, thanks to lodash’s utilities much code that you might have written before becomes boilerplate code. You do not have to care about how exactly your data is transformed, you leave that to lodash and focus on the part of your logic that is really unique to your problem. This is where development work continuously strives for. I mean, we are also usually not doing manual memory allocations anymore. For good reasons.

If this is not convincing enough, remember that all lodash functions are tested by automatic tests and other developers. Your quality assurance can very likely not compete – and if it does, you can (again) skip the time spent for it and just lodash.

My statements might sound trivial, but I have seen much too many developers coding their own versions of functionality available in lodash. That usually results in obscure uses of Array.forEach(), Array.map() and Array.reduce(). Often it is hard to get what that code does because is is neither well written nor named and simply overly complex. It needs refactoring from the the time it is written.

Usually the avoidance of lodash is not done by purpose, but because of not knowing the merits of lodash or that unwillingness to learn new things. Articles suggesting not use lodash might further encourage people to not start to learn and use lodash, what is in general not a good thing.

Code quality in web development benefits by the adoption of lodash (or comparable alternatives) in general and advocating against them without proper differentiation is counter-productive in that sense.

Another example for possible productivity increases I specifically mention because the previously referenced anti-lodash codeburst article favours native functions by falsely stating that lodash functions cannot be composed.

This is a example of how you might create a list of text messages from given data:

This is another way of making your code mode clean (note that _.chain() does not work when using babel-plugin-lodash). And you can even do better by using currying . This is superior functionality compared to native alternatives.

Lodash’s Performance

When lodash was introduced there was a nice video explaining why lodash is faster than it’s native counterparts here. Unfortunately it is not available anymore. Now there are many posts out in the web explaining that lodash is slower. Before discussing those positions my most important claim is:

It does not matter

As long as you do not run very performance sensitive code, lodash will neither slow your code noticeably down nor provide important speedups. While you can feed whole blogs with discussions which one is faster, they simply perform similar enough that you will hardly notice the difference.

The fastest way to work on data are probably simple for-loops, but you usually do not use them anymore for productivity and quality reasons as you should favor lodash functions. In general you should not choose one over the other just because of thinking it is faster.

Most benchmarks lie

While I have seen several performance benchmarks of lodash vs. native implementation, I have never found them credible enough to really call one faster than the other. This is mainly because it is SOO hard to really measure the runtime of the code you want to measure. Some reasons for that are:

  • Different script engines, different versions of engines and different lodash versions. One update of a script engine might boost a lodash implementation while the next one may boost a native function. A benchmark result may only hold true for a specific combination of versions.
  • Native and lodash implementations will behave differently depending on consistency and size you input data. It is ridiculous to run each one of them providing only a tiny array as input, run both versions some thousand times and declare the faster one the general winner. You only prove something for your version combination with your data input and nothing more.
  • Lodash is said to be faster by using short cuts for edge cases. By choosing the right type of input you can either make lodash or native look faster.
  • From the script code you write to the instructions actually executed on the CPU we have several layers of abstraction. They all – especially the script engine – will try to apply various ways of optimizing your code. With synthetic code they might be more successful than they usually are. You might easily see a contender running faster than the other just because of optimization tricks being applied on the synthetic benchmark, that will not apply in real world applications. This is another reason why measured performance results may not apply to your real application.

In the end you probably can only find out which is faster for your specific use case. Probably it is not worth to test and you should aim for better code instead.

Lodash’s real code weight

Lodash is adding weight to your bundles if you run it in front-end. The questionable codeburst article claims that you pull in 24kb of gzipped content by using lodash. But that is only true if you pull in the WHOLE library. Thankfully that is not required at all, but there are the tools to only pick the functions you really use.

  • You can import the modules you use directly by writing
    import map from "lodash/map";

    If you do this for every module in your code, only the used modules will appear in your bundle

  • The more comfortable option is to use babel-plugin-lodash. It provides the same functionality without having you to care about modules. By adding it to your babel setup you can use the more comfortable module import.
  • You might also use lodash-es to reach the same goal. But this may be counter-productive as many npm-packages pull in lodash and using lodash-es may result in both libaries being pulled in your bundle. So better stick with with the babel plugin and even alias lodash-es with lodash in webpack configuration to force the usage of one version only.
  • Another option is to use lodash-webpack-plugin. By using it you can remove functionality from lodash and save compatibility code for older script engines if you only target newer ones. Additionally you can pick the features you do not need and save the bytes. Using it you can reduce the cost of some lodash modules to some bytes! Moreover you can make lodash FASTER by removing features by using this plugin. However you might break your code when you apply it to an existing codebase.

In fact, you can reduce the weight of lodash far below 24kb by applying those optimization. It is probably easy to see that using lodash adds weight to your code but saves bytes when using it. In large projects the code reductions gained will likely outweigh its costs.

In the opposite, there are projects aimed for minimal size where lodash is just too big. If you really need to count every byte you do not want to use lodash but should prefer hand-picking the utility functions you need and optimize them for size.

The question is, when adding lodash to your bundle pays off or not. I provide some numbers about this below.

However, this is very important:

In practice there might also be no additional code weight by importing a lodash module because one of your dependencies already pulls it in. This is actually very likely in a mid-sized project.

Look up the size of each lodash function in my file size table

Why you still might prefer lodash over native functions

After having shown why the major part of lodash is worth to be used, I want to provide reasons why you may even choose it when native alternatives are available. Reasons for still using lodash functions might be.

  • shorter notations that reduce code clutter and save bytes i.e. for _.map(), _.filter() and _.sort()
  • Usable with _.chain() or _.curry()
  • No need to convert between objects and arrays. If your input an object you have to convert that object into an array before being able to use native Array methods on it. Lodash saves you that conversion and helps to reduce repetitive code clutter:
  • No need to check for undefined and null values. Calling .map() on anything else than an Array (i.e. null) will throw an exception, what forces you to do repetitive type checks in avoidance. Not so with lodash.

None of those arguments mean that you must prefer lodash over native, but after having considering them against the costs of using the lodash counterparts you MIGHT.

The costs of lodash

Despite of having stated that performance of lodash does probably not matter, it will still continue to matter for many people. This part shows that even those people should continue to embrace lodash.

In the following I will provide some size and runtime benchmarks between lodash and native versions of the same functionality. I have written before that you should be skeptical about benchmark results. This holds true for this benchmark also. In fact the results I have gathered are influenced by node.js (v8.11), lodash (v4.17), webpack (v4.20), babel (v7.1) and its plug-ins. Additionally you can argue whether my benchmarks are methodically correct. More important, I am benchmarking just ONE function of lodash.

All numbers listed here are taken from the minified and gzipped bundles. The code for the comparission is available at GitHub.

The contenders:

  • Array.map(): Array.isArray(data) ? data.map(item => item[1]) : [];
  • _.map() + function iterator: map(data, item => item[1]);
  • _.map() + string iterator: map(data, "1");

My input data is this “lorem ipsum” array with 180 items.

Runtime

Minimal execution time comparison of 100000 invocations in a loop:

  • Array.map(): 714ms
  • _.map() + function iterator: 700ms
  • _.map() + string iterator: 888ms

What you see is that the shorthand string version is a little slower compared to calling map with a function iterator. And from this number _.map() is minimally faster than native’s Array.map().

But again, do NOT conclude from this that _.map() is generally faster than Array.map(). This is a purely synthetic result for my configuration and with my input array of 180 items. But this data does is not supporting posts pointing out that Array.map() is faster. (Btw. the difference is the same even if you do not check the input with Array.isArray())

First of all this underlines my point that you should NOT choose one over the other for performance reasons. They are very similar.

Code weight

The size of the code is first of all important for front-end applications. The heavier your code the longer it takes the browser to receive and evaluate it.

Now I compare the size of the code in a production bundle created by webpack 4.20 and using babel-plugin-lodash to avoid bundling whole lodash.

The results are:

  • Array.map(): 1350 Bytes
  • Array.map() with _.map() bundled: 7062 Bytes
  • _.map()  : 7043 Bytes
  • _.map() + string iterator: 7047 Bytes

Due to gzip compression results will vary. Nonetheless you can say bundling _.map() is costing about 5700 bytes. In case you can save a type-check using _.map() can save 20 bytes each time you use it (it will cost some extra bytes each time for the file import).

From those numbers you could say that you start to save bytes by using _.map() when using it 285 times in your project. But this is somewhat of a worst case scenario.

In a real world scenario you can probably gain savings by using lodash much earlier as you usually will include many more lodash functions which share dependencies and reduce the cost per function.

The real-world question which you will probably stumble upon is:

“I already use some lodash functions. Should I include and use _.map() in my bundle or stick with Array.map()?”

Exact numbers (again) will of course depend on your project. In my example when having bundle already including the helpful functions _.compact(), _.flatten() and _.intersection() adding _.map() will only add a code weight of 3832 Bytes. This number is not of much use to you, but the point is that the cost of adding a new lodash function do your bundle is probably lower the more functions you already import.

Remember: Some of your dependencies are likely using lodash and force its code in your bundle already. So you might as well just check your bundle analyzer whether the function you want to pull is really not included yet before starting to worry about the added code weight.

Debunk most articles by using lodash-webpack-plugin

After having gathered all that numbers I did a second webpack configuration building the same example bundles using lodash-webpack-plugin in its default settings targeting node.js v8.

The results are impressive.

Runtime using lodash-webpack-plugin

  • Array.map(): 714ms
  • _.map() + function iterator: 548ms

Wow, the optimized _.map() is running considerably faster than the default one. You can still NOT conclude that this _.map() is generally faster than Array.map(), but obviously lodash-webpack-plugin can tune lodash functions even further.

Code weight using lodash-webpack-plugin

  • Array.map(): 1350 Bytes
  • _.map() + function iterator : 1400 Bytes

The cost of adding _.map() to your bundle is reduced to 50 Bytes! I guess it becomes very hard to find arguments for preferring native functions now…

However, in contrast to babel-plugin-lodash the lodash-webpack-plugin is more costly. It filters features from your lodash build. The _.map() version build for the experiment above does not support the string iterator shorthand because that feature is removed by the default settings.

By optimizing lodash for size you are removing some of the features that make it so valueable. If you add this plugin to your project you might have to adjust the codebase and other developers might get confused when lodash in your project behaves differently from the lodash documentation.

So you have to consider whether to use this plugin and what features you want to keep and to remove. But if you need lodash to become smaller and faster, this is a tool the anti-lodash advocates miss.

Conclusion

There are reasons not to use lodash, but all general advise against it needs to be questioned. Embrace lodash when it increases your productivity and readability of you code.

It is still fine to us it even for functionality available natively. If you really have to optimize lodash for size and speed, you can do it with to lodash-webpack-plugin.

Lodash code weight for each function

This is a measurement how much each lodash (v4.17) function weights on its own after being minified and gzipped. As the functions share dependencies you will likely have a much lesser impact the more functions your project already includes. So the practical use of this table is limited but nonetheless interesting.

My data source for this file size table can be found here.

Lodash Function Bytes (minifed and gzipped)
isNil 496
isNull 496
isObjectLike 499
isUndefined 499
defaultTo 501
eq 502
head 505
isObject 509
first 512
last 517
isLength 531
compact 541
castArray 546
sum 574
mean 600
sortedUniq 600
initial 602
negate 602
tail 603
isBuffer 782
isWeakSet 820
isSymbol 821
isNumber 823
isBoolean 829
isString 839
pullAll 852
pullAllWith 857
isFunction 872
isArguments 883
isArrayLike 936
isPlainObject 946
isArrayLikeObject 958
isElement 967
toString 998
isError 1010
uniqueId 1019
toNumber 1029
isDate 1032
isRegExp 1034
isArrayBuffer 1039
replace 1046
parseInt 1058
isNil 1073
escapeRegExp 1074
isNull 1074
isUndefined 1076
gte 1079
lte 1079
flatten 1082
defaultTo 1085
eq 1085
gt 1085
lt 1085
flattenDeep 1087
head 1089
isObjectLike 1093
add 1094
divide 1094
multiply 1094
subtract 1094
toFinite 1094
last 1106
clamp 1112
toInteger 1117
isObject 1118
escape 1121
first 1121
isLength 1126
unescape 1132
sortedIndex 1134
sortedLastIndex 1136
compact 1143
unzip 1154
inRange 1167
sortedLastIndexOf 1178
sortedIndexOf 1181
castArray 1184
toLength 1187
after 1192
toSafeInteger 1195
before 1208
once 1217
isTypedArray 1240
take 1245
drop 1250
unzipWith 1250
nth 1251
dropRight 1255
takeRight 1255
times 1267
isNative 1273
initial 1283
sum 1283
indexOf 1287
tail 1291
lastIndexOf 1300
sortedUniq 1330
negate 1346
startsWith 1365
mean 1373
endsWith 1384
ceil 1393
floor 1394
round 1394
flattenDepth 1405
zipObject 1418
lowerFirst 1450
upperFirst 1450
debounce 1452
capitalize 1466
throttle 1504
isWeakMap 1508
range 1511
rangeRight 1514
slice 1523
random 1550
chunk 1566
trimStart 1568
trimEnd 1571
trim 1599
repeat 1631
words 1636
defer 1677
isMap 1719
isSet 1720
deburr 1763
attempt 1824
zip 1861
keys 1874
delay 1880
padStart 1919
padEnd 1921
values 1932
zipWith 1936
pad 1940
pull 1955
conformsTo 1958
functions 1960
split 1978
forOwn 1992
forOwnRight 1995
isBuffer 1995
nthArg 2016
sample 2021
size 2037
memoize 2040
invert 2067
shuffle 2096
forEach 2121
forEachRight 2124
each 2131
eachRight 2133
isWeakSet 2155
isBoolean 2170
pullAll 2170
isNumber 2173
isSymbol 2173
pullAllWith 2181
isString 2235
truncate 2268
isEmpty 2296
isFunction 2296
isArguments 2372
toPlainObject 2417
uniq 2429
uniqWith 2444
create 2492
toPath 2494
isArrayLike 2497
kebabCase 2501
lowerCase 2502
snakeCase 2502
upperCase 2503
sampleSize 2515
isPlainObject 2537
propertyOf 2607
get 2614
result 2620
isElement 2631
isArrayLikeObject 2648
property 2654
toNumber 2666
toString 2667
uniqueId 2747
unset 2750
isError 2755
flow 2757
flowRight 2759
defaults 2769
replace 2785
escapeRegExp 2810
parseInt 2811
toFinite 2811
without 2821
has 2837
hasIn 2840
startCase 2841
camelCase 2842
gte 2878
lte 2878
set 2879
setWith 2896
sortedIndex 2897
sortedLastIndex 2900
toInteger 2902
intersection 2911
isDate 2911
gt 2912
lt 2912
isRegExp 2915
isArrayBuffer 2925
zipObjectDeep 2937
flatten 2938
flattenDeep 2944
clamp 2949
intersectionWith 2952
update 2969
updateWith 2980
escape 2990
toArray 2997
unescape 3007
add 3026
divide 3026
multiply 3026
subtract 3026
sortedLastIndexOf 3027
inRange 3035
sortedIndexOf 3036
difference 3068
after 3093
differenceWith 3098
toLength 3107
before 3119
toSafeInteger 3122
invoke 3127
union 3133
methodOf 3140
method 3141
unionWith 3164
once 3185
take 3202
drop 3217
dropRight 3223
takeRight 3223
at 3266
nth 3290
unzip 3298
isNative 3317
times 3330
lastIndexOf 3393
xor 3416
indexOf 3450
xorWith 3451
isTypedArray 3595
startsWith 3660
unzipWith 3665
debounce 3671
endsWith 3681
pick 3690
pullAt 3706
ceil 3709
floor 3710
round 3710
flattenDepth 3817
zipObject 3915
throttle 3936
lowerFirst 3957
upperFirst 3957
capitalize 4045
range 4070
rangeRight 4072
slice 4125
chunk 4167
random 4179
isWeakMap 4226
words 4245
invokeMap 4261
trimEnd 4414
trimStart 4417
repeat 4535
trim 4551
merge 4572
mergeWith 4575
flip 4640
defaultsDeep 4651
defer 4659
isEqualWith 4723
deburr 4733
isMatch 4907
isMatchWith 4916
isMap 4980
isSet 4980
clone 4986
cloneDeep 4988
cloneWith 4992
cloneDeepWith 4997
conforms 5073
delay 5265
attempt 5322
zip 5459
pull 5616
nthArg 5634
padEnd 5667
padStart 5667
pad 5729
zipWith 5730
split 5798
keys 5864
sumBy 5954
meanBy 5970
sortedUniqBy 5976
findKey 5983
findLastKey 5985
minBy 5987
maxBy 5997
size 6049
takeWhile 6055
dropWhile 6057
dropRightWhile 6059
takeRightWhile 6059
invertBy 6069
filter 6070
sortedIndexBy 6078
sortedLastIndexBy 6080
map 6122
transform 6123
conformsTo 6128
reduce 6133
reduceRight 6135
reject 6139
partition 6145
every 6151
values 6161
remove 6170
pullAllBy 6172
some 6177
functions 6185
keyBy 6204
countBy 6213
groupBy 6214
flatMap 6243
flatMapDeep 6247
findLastIndex 6263
uniqBy 6274
cond 6336
findLast 6341
pickBy 6351
forOwn 6363
forOwnRight 6365
overSome 6430
orderBy 6442
over 6447
matches 6448
overEvery 6451
overArgs 6458
flatMapDepth 6505
sample 6512
invert 6638
kebabCase 6680
lowerCase 6680
snakeCase 6680
upperCase 6680
intersectionBy 6684
truncate 6688
memoize 6693
differenceBy 6726
forEachRight 6743
forEach 6745
shuffle 6764
eachRight 6776
each 6778
unionBy 6794
xorBy 6989
matchesProperty 7042
isEmpty 7272
toPlainObject 7526
create 7846
startCase 7970
sampleSize 7987
camelCase 8067
flow 8081
flowRight 8083
uniq 8170
toPath 8173
uniqWith 8213
result 8520
defaults 8539
propertyOf 8555
get 8560
property 8726
unset 9018
set 9285
setWith 9321
hasIn 9334
has 9369
zipObjectDeep 9422
toArray 9511
without 9583
update 9665
updateWith 9701
intersection 9792
intersectionWith 9944
invoke 10233
method 10282
methodOf 10282
difference 10398
differenceWith 10529
union 10550
unionWith 10696
at 10796
xor 11474
xorWith 11620
pullAt 12081
pick 12153
flip 13019
invokeMap 14379
merge 15790
mergeWith 15794
isEqualWith 15964
defaultsDeep 16051
isMatch 16495
isMatchWith 16524
clone 18311
cloneDeep 18317
cloneWith 18345
cloneDeepWith 18351
conforms 18605
sumBy 20144
sortedUniqBy 20183
meanBy 20236
maxBy 20252
minBy 20252
takeWhile 20352
dropWhile 20355
dropRightWhile 20358
takeRightWhile 20358
sortedIndexBy 20366
sortedLastIndexBy 20369
findKey 20405
findLastKey 20407
invertBy 20623
filter 20683
map 20713
remove 20804
reduceRight 20840
reduce 20842
transform 20875
findLastIndex 20888
some 20910
pullAllBy 20912
partition 20940
reject 21021
every 21027
flatMap 21125
flatMapDeep 21131
findLast 21156
keyBy 21181
uniqBy 21188
countBy 21238
groupBy 21246
cond 21406
orderBy 21632
pickBy 21702
overArgs 21754
flatMapDepth 21789
over 21856
overSome 21856
overEvery 21972
intersectionBy 22593
differenceBy 22725
matches 22783
unionBy 22910
xorBy 23516
matchesProperty 24846

4 thoughts on “Why you should (still) embrace lodash

Add yours

  1. Thank you for writing this. I’m tired of people bashing lodash without fully understand it’s purpose and the problems it attempts to solve. You’ve summed it all up very concisely.

  2. “It does not matter” when talking about performance.

    I strongly disagree. You might not notice a difference in small parts. But when you have more complex system, user experience won’t be as good when those small parts are put together. Also it’s pain to filter out all parts of big system that makes it slow. So it’s always better to take care of performance in the beginning of your project. Also, there are things that you can feel while browsing, but you won’t notice them. Performance is one of them.

    “As long as you are not living in a legacy environment without babel or have to work with developers not willing to learn ES6, there is indeed not much reason to use them.”

    I strongly disagree. Lazy people are never the reason not to use future technologies and make your projects better. If you have developers that are unwilling to learn, find the ones that will.

    There is a reason why babel exist – so you can use future features in your project today.
    Also there is a reason why those features are present in babel – so you won’t need additional bloat code and libraries that makes your project bundle heavier. No matter that those libraries are pretty small in size. When you sum all of them up, then you’re starting to notice a difference.

    This is just unpopular opinion and some food for thoughts.

    1. Many thanks for your comment.

      I think you got me partially wrong. I strongly agree that performance matters. But I am sure that the difference between lodash or native will not matter for performance in 99% of times.
      90% of the code we write is irrelevant for performance because it is executed often enough. There are performance critical parts. But even in those performance critical parts, it is in the overwhelming amount not cases not relevant what *.map() or *.filter() function you use, it will be much more relevant what you do within the iterated function. In the rare cases it really does matter, you will need to measure what is faster for you and should not rely and bad benchmarks like mine (and many others out there).

      Concerning your opinion about bad developers – in practice I had to find ways to work with bad developers at least for some time. They are out there and there too much of them for being able to just quit and go away whenever you meet one. At least I have to consider many additional factors when considering whether to stay somewhere or not.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: