Video details

React Chicago August 2022 -"Improving React Performance using UseMemo and UseCallback"by Bruce Smith

React
08.31.2022
English

"Improving React Performance using UseMemo and UseCallback" by Bruce Smith
One often missed area in React is performance. Sometimes we have to massage data, process inputs or make calculations on the fly; these are all normal things to do in a React application, but if done carelessly they can cause a performance nightmare. We'll go through a scenario where we look into the UseMemo hook to help us avoid expensive operations from running unnecessarily, and then we'll implement UseCallback to avoid re-rendering child components.
Bruce Smith is a Technical Architect at nvisia and has over 10 years of experience in web development with over 6 years working with Node and React. Bruce specializes in front-end to back-end integrations and is passionate about quality software with well written tests. When he's not working, Bruce enjoys playing ice hockey, golfing, and riding motorcycles.
We are actively looking for speakers! Reach out
at [email protected] you would like to give a talk

Transcript

All right, so let's jump into it. So I'm going to talk a little bit about performance with your application. There's really three things I kind of want to cover. And the first first one is being cognizant of how often your components are rerendering. Right? A lot of times as developers, we write something, we hook up, we spend a lot of time hooking things up, and then it works. So everything must be good, right? Well, what you may not know is when the user actually starts interacting with it and stuff, you might have 500 renders in like 30 seconds or something. And React does a really good job of updating the Dom and all that stuff. But it's kind of behind the scenes and you don't see it until it gets into production. And then it's like, oh. And then you go in as a developer, you look at it, what the hell is happening? Why is it rerendering so many times? It's a glitch. So that's the first thing I really want to cover is just I got a kind of contrived example of components just rerendering like crazy. The second thing I really want to cover is the memorization functions that come based out of the box with React. So there are several things that we can use, several tools, and you can really kind of control to a granular level how often and when you want your components to actually render. And then the last thing I kind of want to cover is just some suggestions that I have, just general kind of tips for making your React applications even better, beyond the code necessarily. So that's what I'll get into today. So my name is Bruce. I'm a technical architect here at Invisia. I've been here like a little over a year, and it's been awesome. I've had the chance to work on some really cool projects and deal with some challenging situations. So let's go ahead and jump into it. I worked on a project last fall, I think it was, and it was a multi sequel project. So we had AWS here on the back end and we had a React project on the front end. And we had these users that would write these SQL queries, shoot it off to the lambda function, essentially. And then that would go query a bunch of RDS databases. It could query one, it could query like 200. You didn't know, depending on what the user selected. Those users, man, they do crazy stuff. But this was cool because the user would make the sequel query, make a connection to the socket, to the API gateway, which created a webocket connection that would invoke a lambda function to do all this foo. And it was using node streaming, and it would stream back through the socket connection, the results in real time. So it was kind of a cool challenge set up and kind of complicated at the same time. Like overly complicated but it was the requirement of the business. So basically what would happen is every time we would get a response back from the lambda function, which was just streaming data, so it was really sending records one by one. The components would be rendered and they had this really big React app and the guy that built the app, he was rock solved, knew exactly what he was doing. And this was basically a new screen in this app and it looked really good and all this stuff. But there were some challenges. The user could write a query that would return tens of thousands and actually millions of records. So you can just imagine in real time millions of records coming in over the socket. So what that really meant was, well, the user needed to see the progress in real time because they might have write a stupid query or something and they realize after a few seconds, oh, that's not what I wanted. So they need to be able to cancel it. So they'd be like, oh man, this is going to take 45 minutes. I'm not waiting for that, I'm going to cancel that and kind of refine my query more. So that was kind of the requirements and that was kind of a challenge. So we had a big problem. User would click the cancel button and nothing would happen. And then every once in a while they click it and it would work like oh, developers, we love that intermittent revolts sometimes. So for solutions, we used React memo and we used callback. And so basically what we did was we control when the buttons, specifically in this case would rerender. Because really we only need the buttons to rerender when the state of the operation here is changing. It's all the button cares about. Buttons care when it's running, when it's not running, or when it's paused or whatever. It doesn't need to rerender every time we get a record. So now that I've kind of stuffed that stuff up, I've got a slide here just to kind of give the definition. I don't really want to go into that too much. But memorization in React, it basically prevents expensive functions from running on each render. So if you have a component that renders a lot, you might not want to do expensive stuff in there because that could really be a pain for your end user. And then the other thing is we can use it to prevent components from rendering when and this is kind of like heuristics of when you want to use it. So you have a component that renders a lot and you have a component that renders with the same exact props. Or the component is large in size, meaning you might have a list of like million fields or whatever. The component is display only and it always renders the same thing. You don't need to rerender that a bunch of times. Or lastly, there you want to granularly control when it's rendered. So you may only care to render something like 50% of the time depending on the results coming back or 5% of the time or whatever. And you can use these memorization functions to kind of control all of that. So just a really quick look here at some of the things I'm going to be demoing in this presentation. So, first off is react memo. So we can wrap a component in React Memo and React will not render that component if it's given the same props a second or third or millionth time, right? React Useref is also a handy piece. This is technically not memorization but it keeps your component from rerendering. So inside of you and I should have put a little more context, but this is inside of a component. You can declare a variable and when you do React User and you pass the initial value, it actually stores it in an object with the default occurrence. Then you can update that as much as you want during the lifecycle of that component and it will not cause it to rerender. So going back to Ben's example where he had the timer and it kept going over and over, the reason that thing rerendered is because the state was getting updated inside of the callback. So when the state updates, it causes rerender. So if you do that every single time things are rendering, you're going to have an infinite loop. And I'm sure if you guys have worked with Use Effect, you've had that happen at some point, you just forget to the other thing here is React Use Callback and React Use Memo. I bundle those together because they're the same thing. The way you implement them is slightly different but even the docs and reactor does the same thing. So Use Callback and Use Memo can be used in a hook fashion to be able to you can do several things with them. Use Memos typically you do something expensive like sort data, which is what I do in my demo. But other things that might take some time, calculations, things like that and then Callback usually can be something for like a callback function. Or in my example, I'm going to wrap a component in this callback. So the point of this whole thing is if you take anything away, you can kind of use these things interchangeably to control exactly how you want your app to react. So let's just jump into the demo. This contrived example that I have processes auto rentals in the last 90 days. I've got three components. So initially, because I'm a great coder, they rerendered for each record that I process. So if I go through 200 auto rentals, all those components are going to reread 200 times. And then what I'm going to do is use some of the pieces from this slide to improve the performance of the app. And so the improvements will be very obvious and pretty drastic. And hopefully what you think from this is you can kind of use all of these pieces to kind of mix and match them, really. And just every app, we all know it's bespoke for some special purpose. So depending on what you're trying to do, you might use one of them, you might use all of them. Right? So it just kind of depends. So let's just go ahead and go over here and I'll see if my app will start up. Does anybody use Beat before or bite one? Yes. Haven't used it much, but I've always been a create rack guy. All right. Okay, so the screen resolution is not my friend here, but I'll kind of do a minimization here. All right. So here I have like a button. You can process the data, you can pause it, resume it if it's paused and reset it. We have another component here which is the progress bar. And then we have this recent rentals which is just going to be a list of the last. And I have it hard coded. Right now it's 200. And I'll modify that as we go through the demo. This right here, these are how many times this component has rendered. So right now they've rendered one time. So I'm going to go ahead and click the process data button and you'll see all this fun stuff happen. But this is pretty alarming, right? Why are we rendering things that fast? And even the list, there's 200 items in the list, but can someone actually follow that? Can the end user actually keep track of that? No. So we can do quite a few things here. Also notice the amount of time that's elapsed like 6 seconds, right? So I'll just run it again just to show that it's consistent. It'll be six or 7 seconds, something like that. And so what I'm going to do is I'm going to go component by component using these different methods to really cut down on how often the components are going to render. And so at the end of this we should get the time to like a second, but even more polarizing is we'll come in here and we'll make it like 2000 so everything's cool for a couple of records and it starts getting slow. So we'll be sitting here all night, right, if we wait for this stuff. And I want to go to the bar later. So we're not going to do that, but you get the idea. And actually I was lucky to pause the button work there because the action buttons, they're re rendering like mad. So the callback function you're passing in is changing every single time it renders. And so we just have a really bad app here, but with 200 record you would never know it just ran and everything looks cool. So let's make some code adjustments and see if we can fix this. So I'm going to start up here at the action buttons. Again, I only need them to render when the state of the process changes. So if I hit process data, well, first it renders the first time. If I hit process data, the state changes that's two. And then when the job is done, that's three. I should see three renders, I shouldn't see 200 however many records I'm processing. So let's go ahead and look at that. And for just sake of time, I kind of already coded it, but I commented out. So what I'm going to use here is for the progress bar is use memo in this particular case. So what I want to do here, and it's a little bit like I said, this is kind of contrived, but what I want to do is I only want to update the progress bar. Oh, I'm sorry, I'm out of, I want to start with the action buttons. My fault. So the action buttons, again, we only want them when the state changes. So Update Processing State here, let me go down to where I'm calling it. I passed three properties to this. I passed the Processing State, which we just affirmed, only changes like three times. The reset button disabled only changes when the processing state changes. I know that. But update processing state. That changes every single time. So let me show you where I declare update processing state. So this is the action. When you click the button or any of the buttons, it basically changes the state which causes a rerender, right? So when I go to look at that, the Update Processing State function in my parent component, I am declaring it here on line 50. So again, I'm just inside of my function. I'm declaring basically a callback or non click event. And because I'm declaring it in the way that I am, every time I get a new record, this rerenders and this becomes a new value. So if you know JavaScript, obviously you know that if you declare like a function, even if it does nothing, an anonymous function never equals another anonymous function. So what's happening is when this rerenders 2000 times or whatever, this Update Processing State function is always a new value. So because that's the case, if I memorize action buttons and I don't do something about Update Processing State, it's still going to update the 2000 times. So let's go look at the action buttons real quick and I'm going to go ahead and wrap this in memo, which actually it already is. So the question would be then why is it still rendering 2000 times? And I just kind of explained that it's because the Update Processing State function is changing on every single render. So to fix that I'm going to use Useref. So what Useref does, if you remember the previous slide, it makes this equivalent to an object with the current property and then that's set to whatever this value is and I'm not going to ever change that. And I could and it wouldn't update the state, but I'm not going to ever change this because I know that that function is always the same. So this current value is never going to change, even if, even if this container component rerenders a million times. So I know that that's always going to stay the same. So what I'm going to do is I uncommented out, oh, I actually deleted it, which I don't want to do. Oh, yeah, I want to delete that one. So I called this update stateref and so when I pass this into my action button component, I'm going to reference this as current, and that is my function. So now when I come over and I run my application, you'll see the stage changed twice now rendered the first time, and when I click the button, renders again. And now the third time. So I've just saved, I don't know, 198 renders here alone, but you'll notice the second hasn't changed much. And that is because this guy here eats up the majority of the performance. But I also know that with this, even though it didn't save much time, I'm not going to have bugs with the buttons because they're not rendering like man. So even though this wasn't a big time saver, it's still like a headache saver when it comes to production, right? So the next thing I'm going to do is go and I'm going to fix the progress bar. So the progress bar doesn't need to rerender 200 times. Really. I only want it to render for this case 100 times, just one for each percent. I don't need it to render the 22.22 or whatever. I just need 1234. That's all the user really needs to see. And as you can imagine, with that kind of granularity, you could even go crazy. I want only 10% or five or whatever, so you get those options. So let's go back into my container code and let's take a look at the progress bar. I'm going to show you where I'm rendering it here. So I have the percent complete, which is what kind of drives the bar itself. And then I have time elapse. So time elapse updates every time I get a new data record. So every time I get a new data record, I calculate the time between when I got the last one and when I got this one. And that's how I kind of calculate that whole thing. Well, I don't want this to rerender every time timelapse updates, which is for every record. I only want it to be every 1%. So to fix this, I'm going to use metal and I'll kind of explain this code here. And if you guys have questions, just stop me telling me, whatever. So what I'm going to do here is I'm going to use rep again and I'm going to only step the current value when percent complete rounded increments by one. Basically, does this make sense? When I set timelapse to a used rep. Now timelapse has a current value. So every time I get to 1%, I'm going to go ahead and I'm going to update the time of lapse current. Again, this setting the current value does not affect stake. So I'm not going to coerce another rerender, it's just going to increment it. And then the render, this one render will get that change. So then I'll pass in my current down here and so now percent complete is only going to do every one time and time lapse is only going to update when I get to that percent completion round, it changes by one. So in effect now I'm only going to render the progress bar one for every 1%. So let's go ahead and see if that fix works. So now I've rendered the progress bar only 101 times as opposed to 200 times or 50,000 times or a million times or however many records I'm processing. If it's over 100, obviously okay. So I know I've kind of dragged on here, so let's get to the meat of it, which is the rentals here. So this is a list. All the items are down here. You can read them all. There's some cool stuff going on here. Like I sort it by the date and the name and the car color or whatever. So every time that this guy renders, I'm doing some expensive stuff. So let's take a look at just that function alone real quick. So as you can see here, every time that the data list renders, I'm sorting this data source data is expensive. I actually think I have a set time out in there for like 20 milliseconds or something like that. If I made it bigger, you would really see it takes like forever. So if you're doing something expensive every time something renders, you might want to go and use memo to kind of whittle that down. So like I said, the user can't see 200 or 1000 records coming in real time. So what I want to do here is I only want to update it maybe every 50 records. So I'm going to implement a couple of things. I'm going to do use ref again. I'm going to basically set a flag to trigger the use memo function to recalculate its value. So here I'm saying if it's mod 50 of the data length, then update my flag and then I pass that as a dependency to use memo. And that tells used memo sort the data again. So every 50 records I'm going to go ahead and sort the data, the list will update and I'm going to save some expensive function time, right? So let me go ahead and save that and we'll take a quick look at that. So you can kind of see the list only jumping a couple of times now and we've gone down from like five and a half seconds to now like 2.7. So that is like the textbook use of a used memo. Don't do something expensive all the time, only kind of when you want to do it. But we can go a step further with the recent rentals. If we're only going to do the sorting every so many times, why not just only render the list at all that many times? We're still rendering 200 times this long list for the first few records. It's small, but then it gets bigger and bigger. If I were to go and change this again to do a bigger data set, even with all this functionality that we've got, you'll see that we still have a user problem, right? And we can sit here and wait for it, but we're not going to do that. So it's optimized or it's only ordering every 50 records, but it still sucks, right? I'm still rendering this entire Dom element hundreds and going to be thousands of times. So what I'm going to do is I'm just going to come in, I'm going to go back to the 200. Actually, I'll keep it at 2000. And then I'm going to come into the data list and I'm going to say, okay, use memo is cool, but it didn't really do it for me. I'm going to go up a parent component and I'm only going to render the data list component differently. So not as many times. So I'm not going to render that big long list hundreds, thousands of times. So this, as you can see, is a little bit more code, but I'll step through it methodically here. So I'm going to create this data list memo variable and I'm going to assign it to the return of use callback. And I'll go through that in a second. But I'm going to come down here and instead of just printing out the data list, I'm going to use my new variable. So again, used Rest to the rescue. I passed in the data. So initially the data is empty, right? It's just an empty array. And then if I'm at 10%, then I set my flag or whatever. It's the exact same technique I use in the last kind of example. And then that's going to trigger use callback to now rerender the data list. And then I set it to the benchmark data current, which is whatever the data was at 10%. So what I'm doing here is I'm saying, hey, only render the list for every 10% result. So if I have 100 items, it's going to render the list ten times. If I have whatever, it's only ever going to render the list every 10%. So what we're going to see now is a huge performance increase and a little bit of a user experience decrease because they're going to see it jump a little more. But you'd rather sit there for five minutes or be able to see the results after a few seconds, right? So let's go ahead and run this and see what that looks like. And again, I've got 2000 records here. So if I go ahead and process that, you'll see here that the data list only rendered the ten times after I hit the start button and now I'm down to like 30 seconds for all 3000. So that's it for the demo. I know that was a little long, but hopefully it really explains how you can kind of mix and match some of the memorization functions that come out with boxes react. And so lastly, I just kind of wanted to add some recommendations here in terms of just like coding react number one, when you're doing memorization, add inline comments. As you can see, a lot of times it's very bespoke and it's for a custom purpose or you got a specific problem. So add comments because I don't know, I can't remember code I wrote like two months ago and then any other developer that comes in after you, they want to know why you're doing that crazy stuff that you're doing. Unit test is a huge one so there's no reason you can't write unit tests to run data through it and assert the amount of times that your components are rendering. So it is a great opportunity for CBD. I could tell you how many times it's going to render after I implemented the use memo. This should only render three times or whatever. So write your test, write the amount of times it's going to render and then go run it. It should break right? Because it's like expected 2000 to equal three and then go do your memorization and then your test should match up. Is there box way to do that or do you have to declare the variables of track? I don't know of an out of the box way to do it and that actually reminds me thank you of kind of a weird thing. So how many guys use Strict Mode to code with React? So something fairly new and I don't remember exactly, but it's fairly new. It renders the components by sale. So if I were to turn on Strict mode and run that again, you see 400 renders instead of 200. So it's kind of annoying to me anyway. But no, I don't know of a powder. Do expensive operations if you can in your middleware and your sockets don't do them in the components. Try to make your components as view only as possible. I know that's not always available. A lot of times I work with things that are specifically fetching data from remote sources and things like that. I'm not doing game development, I'm not tracking cursor positions or amount of time right on screen and things like that. So if you have those things, your mileage may vary, but if you are using middleware Sagas, things like that to fetch data and you need to massage it in some way. Try to do all of that massagus experiment with debunk functions. So debunk functions are very similar to just kind of wait until something else happens and nothing happens. Get the result. Don't like bubble it up as many times as events come in. I already touched on this one. And then lastly, think outside of the app and a lot of times you are the back end developer or whatever. Go in like implement Pagination or something like that on your back end so you don't have to deal with it in your component level kind of stuff. So try to work with back end product teams. If I go back to this example here, we did fix the problem, but we ended up making further improvements by sending the data in chunks instead of one every time. That's kind of silly, right? So always see if you can think outside the box and not have to do some of these kind of bespoke kind of what I would almost call Kluges in some case to get your components to be more performant when you can solve that before the data even gets to you. So that's what I got. If you guys have any questions, fire me away. Yes. So would you have had your sort function on the back end rather than the front end? Yes, if I could control it, yeah, absolutely. And then a lot of times you're getting it from databases and stuff so you can fill that into the queries and obviously databases are a lot more optimized for stuff like that. So I mean. Like I said. It was a pretty contrived example. But if you ever have to do sorting and things like that. Obviously you have a use case where you have a table and you got like ten columns and you got a sort and the user gets to choose and you can't really dictate that on the back end. So yes and no. Okay. For a unit testing, do you use just as a recommended approach or do you use something else? I like jest, I've used several. Just comes out of the box with create racked up. So kind of I just stick with that because that's kind of what I've used for most of my stuff. Jess to me is just really intuitive. It's easy to use. So yeah, personally I use chess. In your demo you had in the last piece that you optimized one of your callback. Why would you do it, use callback and not use that one for the sake of the demo. So I wanted to kind of demonstrate being able to mix and match. So there's some things you can't quite mix and match, but used callback, I could have used memo and then I could have pulled that off yes. With either of those two. Okay. Yeah. So it's just kind of a mixed match thing. In that case, they're entirely. And we talked a little bit earlier, I believe you did, about extensive depth. So exhaustive depth. Sorry. So your dependencies, everything you use inside of the function, you have to put in like your dependency array or whatever, I just hit it off the screen. But like this array. So there are times here, like I'm touching time elapsed, but I'm not putting it in my steps for use mode because I don't want to use memo to get triggered by time of lapse. I want it to get used by that. And again, this goes back to bugs. Right? So react says use exhausted depth. They highly recommend it to avoid bugs. In their documentation, they have explicit examples of the bugs. But if you're writing unit tests and you know exactly how many times you want your stuff to work, then you don't have to worry about your bugs being there. You should catch those in unit tests. And then a lot of times, too, you won't pass extensive debts, but you're not actually modifying any state or props. Not modifying those, you're just exploring. So the bugs happen when you start modifying state inside of those callbacks. That's when the exhaustive steps really help. Can you scroll down and show where you were convincing how many of the counts? Yeah, so I pulled that off. So I'm kind of doing that in the components themselves. So let me just show you the progress bar. So up here above the function, I'm just declaring it to zero. And then I have this render counter component here. So I rendered that render counter with initially zero. And I increment it every time. And all that component does is just play the name and the account. I was guessing somewhere that you had it last month. Yes. So I just increment it and I mean, that's just poor man's way to kind of visually show the amount of times you're entering components. And that's a great point. Like I have the plugin for Chrome to show your component rendering and stuff like that. Yeah, I was going to ask if it shows up. It does, but I'm using Material I because I'm a really terrible design guy and I like to use components that somebody else has written for my buttons and all that stuff. So Material I like these grids and all these components that I'm using. The Dom structure, there's quite a few things there. So when you start looking at that, you see all of those Dom elements and it's really noisy. So I just wanted to do something for sake of demo. That was kind of easy to see. Four mentor. So if I hit reset, by the way, that's above there. The account still reset. That's why. Really quick follow up question. On the previous question in your container, looks like you had to use memo right there. I'm guessing we're just fine if you use user text instead of using them in this case. Yeah, I could have done that too. Every time it renders, I could have done that as well. We're not saving any value. Correct? Yeah. I just really wanted to emphasize the fact that I want to trigger this based on a change in something and a specific something. One thing that I've kind of used in here a lot is I've used the user. That's a really powerful one because you can update that. You take that as much as you want during the lifecycle component and it does not trigger a reread. Whereas if, like I had done on used State instead, if I wanted to change that, it would cause a reminder right there and then you get into like infinite loops and stuff. Cool with that. I appreciate it. Thank you, everyone.