Conf42 JavaScript 2021 - Online

React is Killing your Performance and it's Your Fault

Video size:

Abstract

React is a great tool for writing dynamic single page applications in a declarative way. But, in order to enjoy the benefits of the declarative programming style of React, under the hood a tree comparison algorithm known as the Reconciliation Algorithm runs on each state or prop update.

While the algorithm is very performant and effective, it does have performance impact, and sometimes stepping out of the reconciliation loop is the better choice.

When writing code in React there is sometimes a tendency to try and solve every problem in the application using React’s APIs, usually by searching for a specific library written with React. For example, you want to implement drag and drop? No problem - just DuckDuckGo “”React Drag and Drop Library”” and pick one of the many libraries out there.

But sometimes, using React’s state and prop updates is not the best choice to handle things. React’s reconciliation algorithm triggering comes with a cost and in order to create fast and responsive web applications we should take that into account.

In this talk we will learn all about the reconciliation algorithm. What triggers it, and more importantly when and how to avoid it.

Summary

  • React is used to basically write complex web applications in a declarative way. But it has to do some work behind the scene at runtime. And even if this work is very efficient, it exists. It can sometimes hurt us if we don't know how it works.
  • We shouldn't really optimize every single thing ahead of time. The better approach is to maybe keep in mind that those stuff happens, but only optimize it when they cause a problem. Sometimes optimizing stuff can actually hurt you.
  • Using react memo can cause bugs in your uI. Another thing we can do is not mutating prop values. Find which component needs to be memoized and wrap it with react memo. Instead of that we can actually decouple the child from its parents.

Transcript

This transcript was autogenerated. To make changes, submit a PR.
You hi Conf 42, thanks for attending my talk. React is killing your performance and it's your fault. So a little bit about me I've been a developer professionally for the last ten years or so, and in the last year I've been a co founder of a company called Lifecycle IO, and we create live, ephemeral environments directly from code. So for every code change, every commit, in every branch, we create automatically a new environment. And the purpose of those environments is to allow stakeholders who are not developers, such as designers or product managers, to have a way to interact and experience with the product at a very early stage, before any deployment to any existing environment like staging or production. So those stakeholders will have a way to give feedback and experience the product at a very early stage like we as developers are used to, since we usually have a local environment running on our computer when we develop. So the same kind of experience just for other stakeholders. So please check us out at lifecycle IO and today we're going to talk about performance in reacts. So let's start by reminding ourselves why do we use react? And I just want to say that I'm going to address mostly examples of browser like the DOM, the document object model. But the same principles can be applied into every implementation of reacts like React native. So react is used to basically write complex web applications in a declarative way, right? So instead of having explicitly and imperatively use the DOM as an APIs and give comments to it, we can describe in a declarative way complex UI elements. For example, this element here is changing every time I click the button. So in order to do that in regular JavaScript, this is actually typescript here, we need to very explicitly, in an imperative way, call the DOM API like document create element and tell it what to do. And instead of that we can use react in a declarative way, and by declarative meaning that we described the outcome we want to use. So we actually use JSX and those tags here and use react state management solution, and we change this element according to the state, which is changed by the button click. So this is a very nice way of writing code, and we all love it. So that's why react is very, very popular. And unfortunately that comes with a price, because to enjoy this declarative way of programming, reacts has to do some work behind the scene at runtime. What react does it make sure that what we described in a declarative way, meaning what we told him the outcome should be, it makes sure it actually happens. And to do that, it comparison between the given state of the Dom and the description we provided it. So the actual comparison runs at runtime. So it has an overhead. And let's have a very quick and very simplified overview of that algorithm that runs every time the app gets updated. Just so we have a sense of what it's doing. But the important thing to take from it is that it doesn't come free, right? It's doing some work. And even if this work is very efficient and it's written in the best way possible, it exists, right? So it can sometimes hurt us if we don't know how it works. So this is a representation of the tree. So a react application is basically a tree of elements, right? So every element can has its children, and each of those children can have their children. So basically it's a tree data structure. Every element has a parent. So this here is the drawing of the current react tree, right? So this can be like the app component, and maybe here is a left pane and this is the right pane, those are buttons or something. Every react application can be described in that way. So let's say an update happened. So when we say an update happened in a react application, we mean that either one of the components prop has changed or its internal state has changed. So that triggers a render and that render function goes across and creates a new tree. And this is our new tree. And we can see that the new tree does not have that c element here, and that element value has changed, right? So react what it does, it takes the previous tree and this current tree and it computes the difference, the diff, right? So the magic is that it only takes the diff and then from it it creates the necessary DOM updates, as we saw in the explicit code example. Right? So it produces only the necessary DOM API calls. So this is great, but it actually needs to run that tree comparison algorithm every time something changes. And as we said, it creates an overhead and it's better to avoid it if we can. So we don't have performance issues, right? So let's talk about applications, and let's start by saying that I things that generally only optimize when you need to. So I think that programmers that are using React should basically know what's going on behind the scene, but we shouldn't really optimize every single thing ahead of time. The better approach is to maybe keep in mind that those stuff happens, but only optimize it when they cause a problem. Because sometimes optimizing stuff can actually hurt you. It can create bugs and can create a complex code and it can, even if they are not done right, decrease the performance, which is not good. Obviously it's the opposite of what we want. And also another thing is that when trying to optimize a front end application, whether it's in the dom or mobile app or any other things, usually the most expensive stuff in terms of what I experienced is stuff that having to do with DOm changes. So it's not like react's fault, it's some other things that we do that causes the performance degradation. So we might want to make sure that we eliminated the other causes of the performance degradation before we start optimizing reacts. And another tip here is using the devtools to investigate. So react has a very good extension, reacts devtools, which you can add to your browser and use that to investigate. And we'll see an example for it later on. So first thing we might want to check when we come across a poorly performant component is checking whether we're running it in dev mode. When we run react apps, we usually do it in development time using a local development server. And that development server, it actually does a lot of stuff behind the scene, like for example to provide us warnings and so on. And that stuff has performance impact. So when we build the application with a production flag, it's way more performant. So this is an example here. I created this application, this component, and what it does, it only creates some random circles with the random colors and random position on the screen. And it does that every frame. So I don't know if you can see it, but it's not running very smoothly. It's not 60 FPS. So we can actually check how smooth it actually is by going over here to the devtools. And I click control P and then FPS. And I have an FPS meter. So yeah, it's not even close to 60, it's 11.4, right. And actually this is entire presentation, it runs in a dev server, right? So this is a development server using webpack, using create react app. So also I build things app ahead of time and serve it using just a very simple HTTP server. So if I go to this exact URL, but in the port of, I think it was 80 80, I get the same thing, but built for production. So I can already see that it's way faster. But let's be scientific about it. And we can see it's like almost twice as fast, maybe not twice, but sometimes it's close to twice, maybe 50%, but between 50 and 100% more frames per second, which is a lot. And it's actually very noticeable, at least in my screen. So first thing I recommend doing, because I always forget that, is checking whether you're running in dev mode. Another thing which is a very common way of optimizing things in react, is using react memo. 1. Second, let me get back to the slide I've been at. Right, so react memo is a way to tell react not to under stuff. And how does that work? If a component is pure, and by a pure component, we mean that given the same state and props, it will produce the exact same thing. So given that what react does, if we encapsulate that component using a thing called react memo, which reacts is giving us, we can avoid renders. Because what it does is every time it gets rendered, it compares the props currently and the previous props. And if those has not been changed, then it just doesn't trigger a render. Because since it's a pure component, we know that only when the props has changed, then it should be rerendered. So if it's not encapsulated using reacts memo, it will rerender it, even though props are maybe the same. So this is an example here. This is a component that it can be dragged, right? I can drag it with my mouse and it's very slow. And let's see why. If I go back to the code here, this is the component. And we see that we have this wrapper component, this div here, which is the things that's being dragged. And inside there's things. Long triggering component with the text. And the slow rendering, as its name suggests, is very slow. I just did an artificial way of making it slow just by doing nothing for a certain amount of time. But it's just for the sake of demonstration. Imagine that you're like computing something or getting stuff on the network or doing I o stuff during the render function. But the thing is that this component is actually pure, right? Because this output only changes by this prop, the change the text prop. So it only changes if the prop, the text prop is changed. So we can actually use reacts memo. So let's do it over here. React memo. And then parentheses save it. Hopefully it broke refresh, right? So as you can see, it's very smooth. Very smooth. We can actually check that again using the FPS. Better. Let's do it real quick. And it refreshed. Now what? Never mind. But we can just see it in the presentation. If I drag it right now, it's very smooth. If I take out the react memo, it should be. Let's update, let's refresh. It should be. Yeah, it's way slower because again, react memo compares this text. And since we update the wrapper every time we drag, which is basically 60 times per seconds, every time I drag my mouth around the screen, it updates this one because it's a child. But this prop never changes. So it's a waste to rerender this entire thing. Great, so let's move on. Another thing we can do is not mutating prop values. So the comparison done by react memo is shallow, meaning that it compares object by reference, right? So if you have a prop called o, which is an object, it compares its reference, it doesn't compare its internal values. So if you mutate internal values, it wouldn't help you. Using react memo, it will actually cause bugs in your uI. So if you're using react memo, you should not change the internal values. So this is an example here. So size is actually an object, right? It has the height and width values. And in this case, since I wrapped it in rec memo, it will actually cause a bug because it doesn't compare the internal values of the size, only the reference of it. So in order to fix that, we create a new object. Every time we change the props, we use the spread operator, we create a new object. And now when it compares the prop, it compares it by reference. Since it's a new object, it will trigger a rerender and will fix the bug. So react memo is great and it helps in a lot of cases, but it's not always the best thing to do. We can sometimes do better than that because in some cases prop are always different. There are components like components that uses children, for example, that don't matter what you do, the children will always under because the children are being created every time by react as a new element. And in that case, the comparison function will always fail, which actually will damage the performance a little bit because it adds an extra layer of checking the props. You can avoid that in that case by making sure that this component is memoized. You can use memo if you want, but if you're doing it like that, then keep in mind that children are always new objects. So using react memo wouldn't actually help in this case. For example, this component here, whenever I click the button, it set this discount, right? And discount. This is a component that accept children, right? So if I click this button, I see that it renders every time, even though I wrap the entire thing in your act memo. So a better thing that we can do sometimes is wrapping decoupling from children instead of using memo. So finding which component needs to be memoized and wrap it with react memo. It can be annoying. Sometimes you need to add a little bit of boilerplate and you need to mess with it. And sometimes the flow of the code is not right. Instead of that we can actually decouple the child from its parents. So in this case, let's see an example. This is the draggable component we saw before. We can use the children property over here and instead of doing it inside, because this slow component is not directly coupled to this draggable, this can be a generic draggable component. We can use it for every kind of draggable thing we want. We can actually plug in here every kind of component we would like. So you shouldn't limit it only to the slow rendering component, right. So it will also, by generalizing it will solve the performance issue since this wouldn't trigger a rerender for the slow triggering surrendering component every time we run it, because the children is passed to it as a prop, right. So for example, I can create const slow rendering draggable and we can take the draggable here and as a child we can provide it with a slow running component. This will solve the performant issue. The reason that I think it's not compiling is because it's a forward ref component, but it doesn't matter, I need to provide it with the ref. Right, like this. But the concept is the same. We just decouple this component from its inner child. So whenever this is being rerendered it doesn't affect this one. Another thing we should keep in mind is we should avoid passing state around. So let's see an example here. This is a component that changes. Actually I think this is. Yeah, slow rendering. Yeah, I want to unoptimize it for a second so we can see that things example should be slower. Right, let's go back to that slide. Here we go. So when I click this button, it renders this drag me component because of the way I programming this component. So let's see how it works. Yeah, here we go. It's actually already optimized, so let me stop for a second. The recording. So another example here is avoiding passing state around. So the idea is that if component is not actually using a state that is being provided by a parent component, we should avoid passing it around since each change of the state triggers a new render. So let me demonstrate what I mean in this case. There is a button here that changes color. It changes this text color randomly every time I press the button. And we can see it's very slow. And the reason it's slow is because I plugged in this component, which is, as you can recall, is a very slow rendered component. So pressing that button takes a long time until things is getting updated. So let's see what's going on in this code. Every time I click the button, it sets the color to some random values. And this is the state of the color, right? And this component is a child component of this entire thing. So every time this state is being changed, this entire thing is being rerendered, including the slow rendering. But in fact, the slow triggering component shouldn't be affected by all of this, because they don't have any connection between them. We can decouple those, we can do like this const, let's say random color, and then we can take this wrap it around, react fragment, use the state, right, and put it over here. Yeah, I forgot to return something. Return. And now we should see, if I go back, we should see that exact same component is being rendered very fast, because now whenever I change the state, it's only using the rendering color component, and the slow rendering is not being affected by the changes of the state. So in summary, react is an overhead when it's providing us with that declarative nice way of programming. It does that by running an algorithm at runtime, and that algorithm has some performance impact. Optimize only when needed, because most of the times those optimizations, they don't matter unless they do. Unless you actually have something that is not working properly in your application. Don't forget to use the production build before you start to optimize. Use react, memo, decouple the children from the wrapper, and keep the state as low as possible as we did in the last example. And that's it. Thank you for listening, and have a great rest of the conference.
...

Assaf Krintza

Co-Founder @ Livecycle

Assaf Krintza's LinkedIn account Assaf Krintza's twitter account



Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways