For a real world improvement, this was the difference of a search endpoint taking 1100ms vs 200ms. The original developer preferred DX with expressive code over user affecting performance and it caused Core Web Vital issues.
This is really interesting. I've been doing spread destructuring simply out of habits acquired from using Redux. But I guess I have to rethink that when using reduce.
This is why ended up using ramda, it allows you to keep the functional syntax with great performance. On a brute force code I needed all the permutations of combinations of words extracted from paragraphs (near 1.5M), the code using reduce will take 4 hours to calculate all, with ramda was 5 secs.
Jack, I think it would be interesting to make a video on how to measure the performance of functions and how to measure that performance. Thanks for your videos.
This is a great tip, Jack! I've also implemented this in performance-sensitive situations. However, I don't always apply this advice because of the no-param-reassign eslint rule which I usually have enabled in my projects. This rule helps avoid mutating object references passed into a function. I think I'd add one point to Jack's video here: this performance-boosting trick has no downsides _because_ the lookup is initialized with {}, and not an existing object reference. If you're in this situation, only _then_ is it permissible to apply this approach and ignore the rule. Cheers!
@@jherr I don't blame ya! Neither do I. The lint rule just discourages me from consistently applying this tip and I thought I'd help explain it. Thanks again for your work making videos Jack.
Yes, I have gotten burned using an assign function in a reduce. The spread operator may seem more functional, but “mutating” the accumulator by setting the key is much more efficient.
Didn't realize the performance was that bad between them lol. I believe I've always done the good version but that was more by luck than thinking about performance I think 🤣
I think mutating the object you're creating still abides functional programming principles since you're not mutating the function arguments, so the function is still pure. I think it's ok that the reducer's callback is not pure since it's used once and in place
If you don't plan to consume the output right away, for loop is also a great option. const result = {}; for(let person of people){ result[person.id] = person; }
Nitpick. Technically this isn't about destructuring assignment but the spread syntax being used to *create* object instances. Destructuring uses the rest syntax (which gathers properties rather than spreads) which looks exactly like the spread syntax. The spread syntax expresses is the opposite of the rest syntax used in destructuring.
pretty good stuff. working on some performance issues right now and noticing these patters all over the place. Not sure if this is my silver bullet, buttt very good to keep in mind.
A spread inside a reduce is going to be a square complexity, not cube. There is one loop in the spread operator that has to copy every property of the lookup object into a new object, and the other is the reduce. Two nested loops = square.
Right, but I didn't see anything about destructuring (the title) in the video. It was about the performance effect of using the spread operator for returning a new object on every iteration.
For the second example i would have used the map function it eliminates the need to create a new array on every loop. The first example okay. thank you great tutorials. i do love them.
I wonder if there's an eslint rule for this. Because object spread is certainly preferred in my codebases to avoid mutations (and to help with change detection). It would be nice to highlight cases inside reduce methods only...
3:40 are they semantically the same tho? Option A ur making a new object, option B ur mutating lookup. It's just that there is no danger in mutating, as lookup only exists within the scope of the reduce-function.
There is no semantic difference. You still get a new object or array either way. It’s just a question of how many objects are created and destroyed in the process of generating the result.
I could have sworn that you once had a video comparing the performance of array copying with spread, for loop and other methods. Has it been deleted for some reason?
No one should be surprised when polyfills cause a performance hit. That's part of the trade off, I think, for wanting a new feature without fully supporting it. Thx for the video!
Even without polyfills this is a performance hit, because for an array of N items, with a desired output of 1 object or array, you are creating N+1 interstitial objects or arrays, all with increasing size, and then discarding N of them.
@@jherr Sure. I suppose there are a lot of assumptions here, though. Older versions of JavaScript may be due to older browser (older computer? Maybe a corporate limitation?). I am also assuming if you can use spread operators, your system should be able handle what spread is doing under the hood. Hope you don't mind my comments. Thanks for your videos, Jack!
@@MrPlaiedes Don't mind the comments at all. Keep 'em coming. That being said, I ran the numbers for the performance on Node 16, which is handling the spread operator and those are the results I got. The issue really is the N+1 problem. You want 1 object (or array) and instead you are creating N+1 temporary objects (or arrays) and then throwing away N of those and keeping just the one. And each of those temporary objects (or arrays) is a complete copy of the previous one. So they get bigger and bigger and slower and slower as you go.
If you use ++i a for loop is very slightly faster, but you still have a raft of possible bugs that you don't with for(of) and for(in). The traditional for loop is good when you need precise control of the looping constraints, if your intention is just to loop through every item us for(of) or for(in). Those make your intention clear,.
I am wondering why did you not use performance.now() And if my understanding is correct the reduce was made to make sure you can mutate the accumulator
That's also pretty nasty in terms of garbage collection. I know, the v8 scavenger doesn't have that much problems with objects that die pretty much immediately, but it's still something that might trigger a minor gc cycle early on, and I'd recommend avoiding that, especially during animations or game loops.
Coming across this I wonder. The spread reducer returns a new array but the other Returns the array passes by refrence into the accumulator. This may result in issues in some cases
Maybe, but I think if you understand references enough to use a form of reduce where you mutate the referenced object or array as opposed to continuously creating new objects or arrays, then you'd probably know to control the provenance of the original object or array.
I believe Ikea had that issue where you can't checkout 1000 items from the basket, site would be simply unresponsive. They have been losing money because of that simple mistake ;-)
Hahah, ok. BTW, thank you for the video idea. I forgot to mention you in there. But yeah, you are right, this is mostly something you are going to run into on the node side crunching bigger datasets.
Does this anti-pattern also apply to map? Would either of these be considered anti-patterns? const offices = officeLocations.map(({ id, title, address, phone, fax, slug }) => ({ id, title, address, phone, fax, slug })) or const offices = officeLocations.map((office) => ({ ...office }))
Map doesn't have the same unexpected behavior, because you are creating objects 1-to-1 to match the array. Where in these reduce examples you are creating a single object or array, and with each iteration it's getting bigger and bigger so the copies are more and more expensive as the array size grows.
You're almost there. Now get rid of the reduce and do a simple loop. Using reduce is totally useless (at least in that case) and you got a function call for each item of the loop, which is slow.
Yes, of course. In this case totally on point, however be patient, in other scenarios mutating objects or arrays gets you into trouble. So I think it makes a lot of sense to default on spreading as this is more of a performance optimisation
which extention do you use to see real time results in the editor? Like when you put you cursor on a object, editor shows a result in the right side of that line??
In JavaScript, if you use this anti-pattern on N items you'll get N copies of the output object (or array) plus the one output object (or array) that you actually want. And each copy will be successively larger than the last. Scala has referential data structures so an array can say; "I'm just this array over here, but with this added value." So instead of having N arrays you are basically creating a linked list of related arrays. Still probably not as fast as simply mutating an object (or array), but that's probably mitigated by it being on the JVM and being compiled.
I made a similar comment a couple of minutes ago, so apologies if I’m repeating myself. It’d be interesting to compare to something ClojureScript or Elm that should be using similar data-structures in a JS context.
So, essentially, do not return an object or an array from a loop, if you don't really want to create an additional instance of it every single iteration. That should be in the docs)
That makes sense but this is really only relevant if you have really big data. I mean for most people in most scenarios it will only be like some dozens of objects so maybe it is a good idea to keep the more readable code? But again it totally makes sense depending on the volume of data you are handling
Completely agree, I mentioned that at one point that the impact of this on the client would probably be nil because of the smaller data sizes. But if you're making a library, or you don't know the context or the potential maximum size of the data set, then this is something you should be aware of.
A single big data set is of course one way of getting there. But also think in terms of nested small datasets. Loops in loops can quickly that up that big O and bring optimisations like this back into focus.
If you’re in a situation where you care about GC pauses (games etc), that is another reason to be very mindful of unnecessary object allocations. Also, while there’s definitely some truth in it not always being necessary to think about scalability when you know your code only runs on small datasets, I think we have a lot more bloated and slow software than we need in the world and it’s ultimately contributing to data centres etc consuming a lot more energy and having a much higher carbon footprint than they should.
@@xinaesthetic for sure. I thought about writing stuff for games for instance and also in those cases maybe you don't even want immutability at all (in a lot of scenarios at least) ?
@@xinaesthetic Absolutely agree especially on the points of large scale wastages. We generally code with human first attitude but keeping an eye on easy optimisations where we can. Just because we can be lazy doesn't mean we should. On the flip side of that optimisations first is something we haven't 'had' to do for quite some time now and can really slow a project down. Like all things in life it is about finding a balance.
But they are used for different purposes. I don't feel like this is an apples to apples comparison(i mean it is technically but not when you think about the problem you're solving), you use spread when you specifically want to create new objects instead of using the references, such as when tracking state changes.
It is not immutable, but, let's think about what's happening here. You have a set of N elements from which you want to derive one object or array. And you can either create N+1 objects (or arrays) and throw away all of the N in the process, or you can create one. And in both cases you get a new object or array. Does the gain of having N temporary objects that you throw away that are immutable outweigh the benefit of using a mutable process to derive the one output? And with either process the resultant object (or array) is itself mutable (unless you freeze it).
The video is great and at the point, but it's still insane that only one comment mentioned this. If you have a really huge dataset and are trying to reduce it, it's mostly wrong at first place. You either take part of the data out of the dataset then edit, or you just use a more efficient way to store your data so it's performant to edit. And if you are doing something mutable to the original dataset, it could cause unwanted behavior in components which used the same dataset, because although the dataset contains the data didn't change, but the data itself has been changed.
TS's core concept is that you should be able to strip the TypeScript annotations from the code and be able to run the JavaScript as-is. It doesn't make modifications to how your code works. It just checks types.
@@petrtcoi9398 Ah, ok. It would have to understand the semantics of what you are doing. If you are doing a map then a spread is appropriate if you want to create a new object to replace every element in the array. I wouldn't trust any transpiler to make that call.
@@jherr no no... But mainly most configurations for eslint shows a warning saying that a .push in an array is mutating value and probably isn't what you want. What I'm saying is that most of newcomers to js or react (for example) can be led into fixing a .push to the spreading option...
@@jherr Thanks! I'll do some performance test on large arrays myself. Curious at what array sizes the difference is going to be significant (enough). Enjoying your videos!
@@SanderRombouts Here is the code so you can try it out for yourself: github.com/jherr/javascript-quick-fixes/tree/main/destructuring-is-slow/performance-tests But it's pretty common sense, basically to create a single output object for N elements in an array we are creating N copies of the output array each larger than the previous copy and including all the previous data. Depends on your CPU but I was able to get significant slowdown at 100K elements for a single pass, or 100 * 1K elements in multiple iterations.