Video nenà dostupné.
OmlouvĂĄme se.
Is JavaScript Spread a Performance Killer? Quick Fix
VloĆŸit
- Äas pĆidĂĄn 18. 08. 2024
- (I said destructuring when what I'm really doing is spreading, same performance impacts apply regardless of the terminology.)
Using spreading with Array.reduce is a powerful pattern, but it can also be a performance killer. Let's learn what causes the performance issues and how to get around it while still keeping the same powerful functionality.
Code: github.com/jhe...
đ What's my theme? Night Wolf [black]
đ What's that font? Operator Mono
đ I'm a host on the React Round Up podcast: devchat.tv/pod...
đ Don't forget to subscribe to this channel for more updates: bit.ly/2E7drfJ
đ Discord server signup: / discord
00:00 Introduction
00:35 Object speading performance
05:45 Array speading performance
08:06 Why do we do this?
09:10 Outroduction
Thank you for watching this video, click the "SUBSCRIBE" button to stay connected with this channel.
#javascript #quickFix
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.
Wow I always thought that background was real haha
It is actually my office. I just use a plate that I shot at the day in summer, and so now I can use it in the dark of fall. ;)
@@jherr damn that is an incredibly nice environment for an office!
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!
Honestly, I never even think of initializing reduce with an existing reference.
@@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.
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.
I always destructture with spread, but now I'll think twice before using spread, thanks đđ»
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.
This is amazing, More efficiency tips like this would be awesome short videos to share with friends.
Thanks Jack, That was enlighting, Would love to see more videos about performance from you.
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 đ€Ł
Your voice and way of explaining makes me want to get up early and always keep learning.
Thank you :)
Awesome content jack, thanks. Waiting for your js objects details series
I would have done this: Object.fromEntries(people.map(person => [ person.id, person ]))
I don't know how efficient it is, but it's very readable.
That's a good one, and it should be very efficient.
Nice idea
I had no idea about this performance hit with destructuring. Thanks for sharing.
This answers the questions I had on your other reduce video. Thanks!
I'm still calculating the amount of times I've done this using ...spread . đł Thanks Jack đđŒđđŒ
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
Immutability kills performance & blows up memory.
react-table had this issue (primegen has a video on this)
Great videođ
Thank you Jack, I enjoyed this video at 1:00 AM eating an instant ramen
I love you content. Thanks for sharing your knowledge. Will share your video to my colleague so you can get more subscriber.
Now I finally got something new on you tube Thank man
I would have never done that spread but it was insightful. Thank You.
Perfecto!, just corrected a couple of pieces of code with this smart fix. Thank you Jack!
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.
You are correct and fixed in the title and description.
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.
THANK YOU
applied this immediately
Thanks as always for a great video Jack!
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;
}
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.
Thanks man, Iâll be more cognisant using spread operator now
this was a great video, thanks
Not only more performant. But it's easier to read aswell.
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.
That is true.
Great point!
Great video Jack! Thank you!
Awesome !! đđđ Thanks for Sharing ! đ
You made me cry because I tend to write in a functioning style
Really helpful.
Great information Jack, Thanks
Very intresting... thanks for info
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.
Great video! Thanks!
Love your content Jack!
Good stuff sir, love this content
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.
@big mistake with option 2, you're mutating the object that is passed in as the second argument {}
I would like to see the best useThrottle function in react!
Thanks!
Nice explanation, thank you!
Thank you so much
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...
Very useful
Concerning, I always use spread
Interesting. That is actually very helpful, thanks!
Very intersting, thanks a lot
Thank you! Thank you! very useful
Really useful one
Thx â€
Great content. Thank Jack
Good to know!
ê°ìŹí©ëë€. ì ìœëìë ìŽë° 돞ì ê° ë§ì ê±° ê°ìì.
Thanks sir!
Lovely Video
nice tip, thank you Jack
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.
Is there a list of the extensions you use for VS Code?
This one is Quokka. But yeah, I'll be doing a video on that.
React use that to update states a lot.
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.
What is the name of the extension used for previewing the results?
Quokka.
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
Sloth and yes. :)
Isn't a for loop clearer and more performant? I mean besides the slick one-liners, what purpose do they serve that for loops don't?
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,.
great video thank you :D
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?
I haven't deleted many videos, and I certainly don't remember deleting a basic JS video like that. I've covered it a bunch of times though.
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 really love your content, I wanna learn the architecture thing of an application. Any recommendations or tutorials?
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.
@@jherr thanks to you for the channel! Learned a lot from your videos :-)
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)
Yeah. Don't make JS building an object that you are probably just going to throw away. :)
Just look at the transformation priority premise: reassigning a new object is the worse transformation
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??
Quokka: quokkajs.com/
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.
@@jherr oooohhh i see, mind blown! Thank you
There's no such a thing as anti-pattern with Jack. He's a Senior HelloWorld Developer, what are you talking about?
What are you using that will output the result in file, that you dont need to do console log?
Quokka quokkajs.com/
using `new Map` with `hash.set(key, value)` may even be faster then using object like this
Does anyone know that add on for the preview in the blue text?
Hey, Jack, good show. May I ask how do you get the comparison performance chart on your screen?
That's After Effects. I use a couple of effects on it to swivel it, then do the animations.
Hey Jack, thank you for your prompt response. I am really enjoying the way you explain material, so I'm going to look into one of your courses.
What's the extension you are using??
Quokka.
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.
Question does this only apply to reduce method? I wonder would using the spread operator in a reducer cause any performance issues?
I can't think of any of commonly taught pattern that has this kind of performance issue. But I'll definitely have more of a think about it.
The benefits of the spread operator is immutable. Is the second method immutable?
You create one object and you mutate that. Original data isn't change. There is no down side to that mutation.
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.
It was realy great learning, would you suggest any book which covers such anti patterns in good detail
I think you may have gotten the big O notations the wrong way around.
From my tests its the object that is ^2 and the array is ^3
This is amazing. I never thought that internally it may impact so much. What's the name of that extension which is showing the output on hover
Can you cite a time reference? I think it's just the usual hover.
@@jherr It is on min 3:15. For me its not the usual hover
@@roberthaforsson9687 That's Quokka. It's an extension.
Mr. Jack i looooove your videos. I would love a course recommendation if you know any.
Wondering if you could elaborate on the underlying aspects that make this slow in JS but performant in others like Scala?
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 creating new object/array in every loop is slower than modifying the existing object/array? What a revelation :)
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.
Please, which plugin suggest the output at the end of the line?
Quokka or Console Ninja.
So the fix is to not use the spread operator to create a return object in a loop if you can simply reuse an already created object?
Yes. Simple. But, unknown to some folks, so... a video. :)
@@jherr I believe that most are taken by the immutable lintings. At least in the FE world
@@s3xta13 Confused, are you saying that eslint would fix that for us?
@@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...
@@s3xta13 Ahhhh, gotcha, that would be unfortunate, because in this case you really want to use the much-maligned push.
Great video. But why TS just donât convert spread operator to âfastâ version of code?
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.
@@jherr Sorry. Not correct answer. I mean Babel
@@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 OK. I see. Thank you
Using âquokkaâ? Whatâs that?
It's a VS Code extension that provides interactive workbooks running JavaScript or TypeScript.