Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

revision for lesson 1 and landing page #10

Merged
merged 2 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/react-render-perf/content.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {Link} from "react-router-dom";

import profilerSettingsUrl from "./images/profiler-settings.png"
import whatChangedUrl from "./images/what-changed.png"
import highlightRerenders from "./images/highlight-rerenders.png"

# React Render Perf

The purpose of this workshop is to learn about common issues that can result
react renders taking longer than expected.

## Preliminaries

This tutorial assumes that you have the [React Developer Tools Chrome](https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?pli=1) extension installed. You will need
to make use of the "Profiler" tab in the React DevTools to measure performance
of the components in the exercises. For information on how to use "Profiler"
tab see this [blog post](https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html).

<em>WARNING: The blog post is quite old and parts of the UI no longer exist</em>

### Configuring React DevTools

There are a couple of very helpful settings for React DevTools that you should turn on
before starting the lessons. Once you've installed the React DevTools, click on the "⚛️ Profiler"
tab in the Chrome DevTools. Then click on the gear icon below the "⚛️ Profiler" tab, this
will open up the React DevTools setting dialog. Select the "General" tab in the dialog and
enable "Highlight updates when components render". Whenever a component is rendered, it
will appear with a cyan outline around it.

<img src={highlightRerenders} width="500" />

Switch to the "Profiler" tab in the dialog and enable "Record why each component rendered while profiling".

<img src={profilerSettingsUrl} width="500" />

After you capture a profile, you'll be able to see what triggered a component to re-render
by clicking on the component in the flame chart. You can also click on the different times
a component was rendered at to see what triggered different renders of the same component.

<img src={whatChangedUrl} width="500" />

## Lessons

1. <Link to="/react-render-perf/lesson-01">Memoizing Expensive Functional Components</Link>
2. <Link to="/react-render-perf/lesson-02">Prevent React.Context from re-rendering the whole tree</Link>
3. <Link to="/react-render-perf/lesson-03">Avoid using React.Context at all</Link>
4. <Link to="/react-render-perf/lesson-04">Minimizing re-renders by splitting up large components</Link>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/react-render-perf/images/what-changed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 0 additions & 20 deletions src/react-render-perf/index.mdx

This file was deleted.

9 changes: 9 additions & 0 deletions src/react-render-perf/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Content from "./content.mdx";

export default function TableOfContents() {
return (
<div style={{maxWidth: 800, padding: 8}}>
<Content />
</div>
);
}
68 changes: 48 additions & 20 deletions src/react-render-perf/lesson-01/content.mdx
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
Memoization can be used to avoid unnecessary renders. This is most useful when
the component itself is expensive to render (e.g. the <code>MathJax</code> component in
webapp) or it renders a lot of descedent components.
<em>This lesson is focused on memoization of function components. If you need to memoize a
class-based component you can define the <code>shouldComponentUpdate(nextProps, nextState, nextContext)</code>
method in your class, see https://react.dev/reference/react/Component#shouldcomponentupdate for details.</em>

Memoization of components can be used to avoid unnecessary renders by not re-rendering the
component if the props haven't changed. This is most useful when the component itself is
expensive to render (e.g. the <code>MathJax</code> component in webapp) or it renders a
lot of descedent components.

Memoization works by saving the rendered output of the component based on the
props that are being passed in. Often times props will appear to have changed
when their actual values haven't. In JavaScript, two objects with the same
properties are considered different objects. Similarly, two functions with the
same implementation are considered differen objects.

In order for memoization to have the desired benefit, we don't want the
component to rerender if there are only superficial changes to props.
<code>React.memo(Component, arePropsEqual?)</code> is used to memoize components. If
<code>arePropsEqual</code> isn't provided it does a shallow comparison of props and as
such isn't able to determine when a prop that's an object or function are equal or not.
In order for memoization to have the maximum benefit we need some way to determine if
objects have the same contents and if callback props haven't changed.

<code>React.memo(Component, arePropsEqual?)</code> by default does a shallow comparison of
props and as such isn't able to determine when a prop that's an object or function
is the same or not. We can pass a custom <code>arePropsEqual</code> function to override
We can pass a custom <code>arePropsEqual</code> function to override
that behavior. To keep things simple we use a third-party library called
<code>react-fast-compare</code> which provides a function that does a deep comparison of
objects.
Expand Down Expand Up @@ -82,22 +88,41 @@ const ParentComponent = (props: Props) => {
}
```

If the <code>ParentComponent</code> is a class-based component, there is no need to memoize
function props that are pre-bound methods. This is because the method never changes
for the component instance. If the prop is an inline function though, e.g.
<code>onClick=\{() => \{ ... \}\}</code> it should be converted to a pre-bound method, see the
example below.
## Parent is a class-based component

If the <code>ParentComponent</code> is a class-based component, functions passed as props
to child components should be pre-bound methods. A pre-bound method never changes for the
lifetime of component instance. If the prop is an inline function though, e.g.
<code>onClick=\{() => \{ ... \}\}</code> a new version of the function will be created
each time the component is rendered. Inline functions being passed as props should be
converted to pre-bound methods as shown below.

**BEFORE**
```ts
import ChildComponent from "./my-component";
class ParentComponent extends React.Component<Props, State> {
componentDidMount() {
fetch(QUERY).then((result) => {
this.setState({result});
});
}

type Props = {
user: {name: string, id: string},
};
type State = {
result?: Result<typeof QUERY>,
render() {
return <ChildComponent
user={user}
// A new function will be pass to `onClick` each time
// `ParentComponent` is rendered.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// A new function will be pass to `onClick` each time
// A new function will be passed to `onClick` each time

onClick={() => {
if (this.result?.data) {
// do something with the data
}
}}
/>
}
}
```

**AFTER**
```ts
class ParentComponent extends React.Component<Props, State> {
componentDidMount() {
fetch(QUERY).then((result) => {
Expand All @@ -113,7 +138,10 @@ class ParentComponent extends React.Component<Props, State> {
}

render() {
return <ChildComponent user={user} onClick={handleClick}>
return <ChildComponent
user={user}
onClick={this.handleClick}
/>
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion src/react-render-perf/lesson-01/exercise/child.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type Props = {
onClick: () => void;
};

export function ChildComponent({circle, onClick}: Props) {
export default function ChildComponent({circle, onClick}: Props) {
return (
<div>
<h1>Child Component</h1>
Expand Down
6 changes: 3 additions & 3 deletions src/react-render-perf/lesson-01/exercise/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {useState} from "react";
import * as React from "react";

import {ChildComponent} from "./child";
import ChildComponent from "./child";

export default function Exercise1() {
const [count, setCount] = useState(0);
const [count, setCount] = React.useState(0);
const handleClick = () => {
setCount((count) => count + 1);
};
Expand Down
2 changes: 1 addition & 1 deletion src/react-render-perf/lesson-01/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function Page() {
<div className={styles.container}>
<div className={styles.header}>
<Link to="/react-render-perf">Home</Link>
<h1>01 - Memoizing Expensive Components</h1>
<h1>01 - Memoizing Expensive Functional Components</h1>
</div>
<div className={styles.column}>
<Content components={{code}} />
Expand Down
9 changes: 4 additions & 5 deletions src/react-render-perf/lesson-01/solution/child.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {memo} from "react";
import * as React from "react";
import arePropsEqual from "react-fast-compare";

import {fib} from "../../shared/fib";
Expand All @@ -9,10 +9,7 @@ type Props = {
onClick: () => void;
};

export const ChildComponent = memo(function ChildComponent({
circle,
onClick,
}: Props) {
const ChildComponent = React.memo(function ({circle, onClick}: Props) {
return (
<div>
<h1>Child Component</h1>
Expand All @@ -28,3 +25,5 @@ export const ChildComponent = memo(function ChildComponent({
</div>
);
}, arePropsEqual);

export default ChildComponent;
8 changes: 4 additions & 4 deletions src/react-render-perf/lesson-01/solution/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {useState, useCallback} from "react";
import * as React from "react";

import {ChildComponent} from "../solution/child";
import ChildComponent from "../solution/child";

export default function Solution1() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
const [count, setCount] = React.useState(0);
const handleClick = React.useCallback(() => {
setCount((count) => count + 1);
}, []);
const circle = {
Expand Down
2 changes: 1 addition & 1 deletion src/react-render-perf/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {BrowserRouter, Route} from "react-router-dom";

import TableOfContents from "./index.mdx";
import TableOfContents from "./index";
import Lesson01 from "./lesson-01/index";
import Lesson02 from "./lesson-02/index";
import Lesson03 from "./lesson-03/index";
Expand Down
Loading