Skip to content

Commit 77ff87c

Browse files
committed
perf
1 parent c3aae9c commit 77ff87c

File tree

5 files changed

+190
-0
lines changed

5 files changed

+190
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
React generally has a very good performance profile, no matter what "thought leaders" may be tweeting about it. If you write React in a way consistent with the way I taught you in this class and the intro as well as the docs, you really don't need to concern yourself too much with "is this going to perform well" because React tends to be really good at making sure average code runs fast enough.
2+
3+
To be entirely honest, it was _hard_ to make non-performant code in React. We're going to have to resort to adding artificial slowdowns in our code because my MacBook could consistenly churn through the code inefficient code anyway, making it hard to see performance gains we're going to get. Nonetheless, these concepts aren't important until they're _very_ important.
4+
5+
> 💡 I am about to show your four different ways to make React perform better in certain circumstances. My **strong** advice for is to not pre-emptively use these. You should wait until you have a performance problem in your UI before you try to optimize it. These tools make your code harder to read and makes bugs hard to track down (since you're getting outside of the normal way of writing React.) Have problems before you solve them.
6+
7+
So where does React have problems some times?
8+
9+
![React tree of five components: app, content, profile, profilepic, and profilename. App has children content and profile. Profile has children ProfilePic and ProfileName.](/images/profile.png)
10+
11+
Imagine a change happens in the Profile component and nowhere else. What happens? Profile will update, and as apart of that, ProfilePic and ProfileName _will also run_ even though nothing changed in them. Why? React isn't sure that the change Profile won't cause some cascading effect in either of those components, and re-runs them just to be sure.
12+
13+
Is this a problem? 99.99% no – most of the time these renders are so fast it's not even measurably different to just let them re-render, particularly if it's infrequent.
14+
15+
Do Content and App re-render? No. Only the children in the Profile tree do.
16+
17+
So what if ProfilePic is particularly expensive to render? Maybe it's an LLM-generated picture and it runs the LLM every time it re-renders. This could be bad: every change in Profile and App would cause re-renders in ProfilePic, even if it's not changing!
18+
19+
This is where three tools are going to come in very helpful: memo, useMemo, and useCallback. These going to give us a finer grain control to say "only re-render when this component indeed needs to re-render."
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
Let's open the `perf` project in our repo and run `npm install`.
2+
3+
We're going to build a markdown previewer! On some laptops this can be pretty slow to parse and re-render to the DOM. On my laptop it's actually fast enough to get through it so we're going to introduce some artifical jank. Your computer may not need it.
4+
5+
I left a long markdown file for you to use as a sample in markdownContent.js. I asked Claude to make some jokes for us. I'd call it a middling success.
6+
7+
Make a new file called MarkdownPreview.jsx. Put this in there
8+
9+
```javascript
10+
const JANK_DELAY = 100;
11+
12+
export default function MarkdownPreview({ render, options }) {
13+
const expensiveRender = () => {
14+
const start = performance.now();
15+
while (performance.now() - start < JANK_DELAY) {}
16+
return null;
17+
};
18+
return (
19+
<div>
20+
<h1>Last Render: {Date.now()}</h1>
21+
<div
22+
className="markdown-preview"
23+
dangerouslySetInnerHTML={{ __html: render(options.text) }}
24+
style={{ color: options.theme }}
25+
></div>
26+
{expensiveRender()}
27+
</div>
28+
);
29+
}
30+
```
31+
32+
- This is the artificial jank. It ties up the main thread so it'll run slower. Feel free to modify `JANK_DELAY`. Right now I have it delaying 100ms so you can see the jank more pronounced.
33+
- I'm also showing the time so can see how often the component runs.
34+
- `dangerouslySetInnerHTML` is fun. It just means you need to _really_ trust what you're putting in there. If you just put raw user generated content in there, they could drop a script tag in there and do a good ol' [XSS attack][xss]. In this case the user can only XSS themself so that's fine enough.
35+
36+
Okay, let's make our App.jsx
37+
38+
```javascript
39+
import { useEffect } from "react";
40+
import { marked } from "marked";
41+
import { useState } from "react";
42+
43+
import MarkdownPreview from "./MarkdownPreview";
44+
import markdownContent from "./markdownContent";
45+
46+
export default function App() {
47+
const [text, setText] = useState(markdownContent);
48+
const [time, setTime] = useState(Date.now());
49+
const [theme, setTheme] = useState("green");
50+
51+
useEffect(() => {
52+
const interval = setInterval(() => {
53+
setTime(Date.now());
54+
}, 1000);
55+
return () => clearInterval(interval);
56+
}, []);
57+
58+
const options = { text, theme };
59+
const render = (text) => marked.parse(text);
60+
61+
return (
62+
<div className="app">
63+
<h1>Performance with React</h1>
64+
<h2>Current Time: {time}</h2>
65+
<label htmlFor={"theme"}>
66+
Choose a theme:
67+
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
68+
<option value="green">Green</option>
69+
<option value="blue">Blue</option>
70+
<option value="red">Red</option>
71+
<option value="yellow">Yellow</option>
72+
</select>
73+
</label>
74+
<div className="markdown">
75+
<textarea
76+
className="markdown-editor"
77+
value={text}
78+
onChange={(e) => setText(e.target.value)}
79+
></textarea>
80+
<MarkdownPreview options={options} render={render} />
81+
</div>
82+
</div>
83+
);
84+
}
85+
```
86+
87+
Alright, go play with it now (you may need to mess with the JANK_DELAY as well as the interval of how often the interval runs). The scroll is probably either janky or it has a hard time re-rendering. Typing in it should hard as well. Also notice that the current render. Re-rendering the theme is tough too.
88+
89+
So how can we fix at least the scroll portion, as well make the other two a little less painful (as they'll only re-renderingo once as opposed to continually.)
90+
91+
[xss]: https://owasp.org/www-community/attacks/xss/
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
When does a componet re-render? It re-renders when its parents have changed, always. But what if we could say "only re-render when your props have changed"? Frankly that's most components, but this is an opt-in pattern for React. Generally, like we said, renders are so cheap that introducing this "memoization" layer just makes it harder to debug. But in this case we need some help, so let's do it!
2+
3+
In MarkdownPreview.jsx
4+
5+
```javascript
6+
// at top
7+
import { memo } from "react";
8+
9+
// wrap function
10+
export default memo(function MarkdownPreview({ render, options }) {
11+
// code
12+
});
13+
```
14+
15+
Now we've told React "only re-render this when the props haven't changed. But it's still re-rendering? Why?
16+
17+
Well, answer me two questions
18+
19+
```javascript
20+
const objectA = {};
21+
const objectB = {};
22+
23+
console.log(objectA === objectB); // is this true or false?
24+
25+
const functionA = function () {};
26+
const functionB = function () {};
27+
28+
console.log(functionA === functionB); // is this true or false?
29+
```
30+
31+
If you run this, what will be the two logs? Go ahead and try it. Copy and paste it into your console.
32+
33+
Did you get two `false`? It's because despite being equivalent in terms of what we've instantiated them with, they are two separate entities. They're two different pointers, if you're familiar with C++ or other languages.
34+
35+
So with our example, despite our functions and objects being constructed equivalently like in our little example here, they're considered **not equal** when React compares them.
36+
37+
Well, damn, okay, how do we deal with that? useCallback and useMemo. Let's go do it in our app.
38+
39+
```javascript
40+
// at top
41+
import { useState, useCallback, useMemo } from "react";
42+
43+
// replace our options and render
44+
const render = useCallback((text) => marked.parse(text), []);
45+
const options = useMemo(() => ({ text, theme }), [text, theme]);
46+
```
47+
48+
- There you go! Now it should be rendering and only re-rendering when needed! 🎉
49+
- Now it gives _the same_ options and _the same_ render each render so the `===` comparison will be true.
50+
- The array following it works just like useEffect - it's essentially the cache key. If one of those things changes in the array, it invalidates the memo/callback and makes it render again.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
## useMemo vs useCallback
2+
3+
What's the difference between useCallback and useMemo? Not much. You could actually rewrite our code to look like this and it'd be equivalent.
4+
5+
```javascript
6+
// render and render2 are functionally equivalent
7+
const render = useMemo(() => (text) => marked.parse(text), []);
8+
const render2 = useCallback((text) => marked.parse(text), []);
9+
```
10+
11+
Notice there's an extra `() => [fn]`. useCallback is only a thin wrapper on useMemo that automatically wraps it in the additional function. It's otherwise 100% equivalent.
12+
13+
## What about strings and numbers?
14+
15+
If you're just passing in strings and numbers to a `memo`ized function, you don't need useMemo or useCallback since those will just pass the `===` test already. It's just objects and functions! So instead of having options as an object, we could have had theme and text as top level props and we could have skipped using useMemo!
16+
17+
That's it! While you should use these tools sparingly, they're handy when you do need them. They let you create a better performance profile when it's important.
18+
19+
## Don't just use memo all the time 🙏
20+
21+
As your potential future coworker, please please please don't just use memo on every function. I know it sounds like a good idea but let me tell you why. When you memoize something, it doesn't re-render when its props haven't changed. You're correct in thinking that is normally true of most components. However I **guarantee** you will start getting bugs of a component not re-rendering when you expect it to, and that is a hard bug to find if it's an errant memo somewhere in your codebase (spoken by someone who has had to debug this.) Just don't do it.
22+
23+
## React Compiler
24+
25+
We talk a bit about [React Compiler][compiler] in the intro course, but let's mention it again here. The function of React Compiler is to examine your code and determine what components could be memoized without causing unexpected behavior. So just what I told you not to do? React is actually going to do this for you free. It's still in a rough alpha but they're encouraging people to try it out.
26+
27+
[See my Intro lesson on it][intro].
28+
29+
[compiler]: https://react.dev/learn/react-compiler
30+
[intro]: https://react-v9.holt.courses/lessons/whats-next/react-compiler

public/images/profile.png

191 KB
Loading

0 commit comments

Comments
 (0)