diff --git a/src/react-render-perf/README.md b/src/react-render-perf/README.md new file mode 100644 index 0000000..0e2bbc8 --- /dev/null +++ b/src/react-render-perf/README.md @@ -0,0 +1,11 @@ +# React Render Perf + +The purpose of this workshop is to learn about common issues that can result +react renders taking longer than expected. + +## Lessons + +1. Memoizing expensive to render components +2. Prevent React.Context from re-render the whole tree +3. Avoid using React.Context at all +4. Minimizing re-renders by splitting up large components diff --git a/src/react-render-perf/index.tsx b/src/react-render-perf/index.tsx deleted file mode 100644 index b80fde1..0000000 --- a/src/react-render-perf/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import {Link} from "react-router-dom"; - -export default function ReactRenderPerf() { - return ( - <> - Home -

React Render Perf

- - - ); -} diff --git a/src/react-render-perf/lesson-01/README.md b/src/react-render-perf/lesson-01/README.md new file mode 100644 index 0000000..e1a3d99 --- /dev/null +++ b/src/react-render-perf/lesson-01/README.md @@ -0,0 +1,129 @@ +# 01 - Memoizing Expensive Components + +Memoization can be used to avoid unnecessary renders. This is most useful when +the component itself is expensive to render (e.g. the `MathJax` 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 want don't want the +component to rerender if there are only superficial changes to props. + +`React.memo(Component, arePropsEqual?)` by default does a shallow comparison of +props and as such isn't able to determine when a prop that's on object or function +is the same or not. We can pass a custom `arePropsEqual` function to override +that behavior. To keep things simple we use a third-party library called +`react-fast-compare` which provides a function that does a deep comparison of +objects. + +```ts +import arePropsEqual from "react-fast-compare"; + +type Props = { + user: {name: string, id: string}, + onClick: () => void, +} + +const ChildComponent = (props: Props) => { + // ... +} + +export default React.memo(ChildComponent, arePropsEqual); +``` + +There is a bit of a gotcha here when it comes to props that are functions. +`react-fast-compare` cannot check if two functions are the same. Imagine the +following scenario: + +```ts +import ChildComponent from "./child-component"; + +type Props = { + user: {name: string, id: string}, +}; + +const ParentComponent = (props: Props) => { + const result = useQuery(QUERY); + + const handleClick = () => { + if (result.data) { + // do something with the data + } + }; + + return +} +``` + +Each time `ParentComponent` renders, a new copy of `handleClick` will be created +even if the `result` from `useQuery` isn't ready yet. We only want this function +to treated as a new function when `result` changes. React provides a hook called +`useCallback` which does exactly that by memoizing the function. + +```ts +import ChildComponent from "./my-component"; + +type Props = { + user: {name: string, id: string}, +}; + +const ParentComponent = (props: Props) => { + const result = useQuery(QUERY); + + const handleClick = React.useCallback(() => { + if (result.data) { + // do something with the data + } + }, [result]); + + return +} +``` + +If the `ParentComponent` 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, it should be +convered to a pre-bound method. + +```ts +import ChildComponent from "./my-component"; + +type Props = { + user: {name: string, id: string}, +}; +type State = { + result?: Result, +} + +class ParentComponent extends React.Component { + componentDidMount() { + fetch(QUERY).then((result) => { + this.setState({result}); + }); + } + + handleClick = () => { + if (this.result?.data) { + // do something with the data + } + } + + render() { + return + } +} +``` + +**WARNING:** +Memoization is not free. It requires memory so you should be picky when deciding +what to memoize. + +| Good Candidates | Bad Candidates | +| ------------------------------------------ | ---------------------------------------- | +| lots of descendants | few descendants | +| expensive to render | inexpensive to render | +| actual values of props change infrequently | actual values of props change frequently | diff --git a/src/react-render-perf/lesson-01/exercise.tsx b/src/react-render-perf/lesson-01/exercise.tsx new file mode 100644 index 0000000..6d31df1 --- /dev/null +++ b/src/react-render-perf/lesson-01/exercise.tsx @@ -0,0 +1,3 @@ +export default function Exercise1() { + return

Exercise 1: Memoizing Expensive Components

; +} diff --git a/src/react-render-perf/lesson-02/README.md b/src/react-render-perf/lesson-02/README.md new file mode 100644 index 0000000..2179bf9 --- /dev/null +++ b/src/react-render-perf/lesson-02/README.md @@ -0,0 +1,3 @@ +# 02 - Prevent Context From Rerendering + +TODO diff --git a/src/react-render-perf/lesson-02/exercise.tsx b/src/react-render-perf/lesson-02/exercise.tsx new file mode 100644 index 0000000..768efba --- /dev/null +++ b/src/react-render-perf/lesson-02/exercise.tsx @@ -0,0 +1,3 @@ +export default function Exercise2() { + return

Exercise 2: Prevent Context From Rendering

; +} diff --git a/src/react-render-perf/lesson-03/README.md b/src/react-render-perf/lesson-03/README.md new file mode 100644 index 0000000..9225467 --- /dev/null +++ b/src/react-render-perf/lesson-03/README.md @@ -0,0 +1,3 @@ +# 03 - Avoid Using Context + +TODO diff --git a/src/react-render-perf/lesson-03/exercise.tsx b/src/react-render-perf/lesson-03/exercise.tsx new file mode 100644 index 0000000..dcf471b --- /dev/null +++ b/src/react-render-perf/lesson-03/exercise.tsx @@ -0,0 +1,3 @@ +export default function Exercise3() { + return

Exercise 3: Avoid Using Context

; +} diff --git a/src/react-render-perf/lesson-04/README.md b/src/react-render-perf/lesson-04/README.md new file mode 100644 index 0000000..5149ef5 --- /dev/null +++ b/src/react-render-perf/lesson-04/README.md @@ -0,0 +1,3 @@ +# 04 - Splitting Large Components + +TODO diff --git a/src/react-render-perf/lesson-04/exercise.tsx b/src/react-render-perf/lesson-04/exercise.tsx new file mode 100644 index 0000000..6759433 --- /dev/null +++ b/src/react-render-perf/lesson-04/exercise.tsx @@ -0,0 +1,3 @@ +export default function Exercise4() { + return

Exercise 4: Splitting up large components

; +} diff --git a/src/react-render-perf/routes.tsx b/src/react-render-perf/routes.tsx new file mode 100644 index 0000000..7084024 --- /dev/null +++ b/src/react-render-perf/routes.tsx @@ -0,0 +1,29 @@ +import {BrowserRouter, Route} from "react-router-dom"; + +import TableOfContents from "./table-of-contents"; +import Lesson1Exercise from "./lesson-01/exercise"; +import Lesson2Exercise from "./lesson-02/exercise"; +import Lesson3Exercise from "./lesson-03/exercise"; +import Lesson4Exercise from "./lesson-04/exercise"; + +export default function Routes() { + return ( + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/react-render-perf/table-of-contents.tsx b/src/react-render-perf/table-of-contents.tsx new file mode 100644 index 0000000..74d1bcd --- /dev/null +++ b/src/react-render-perf/table-of-contents.tsx @@ -0,0 +1,32 @@ +import {Link} from "react-router-dom"; + +export default function ReactRenderPerf() { + return ( + <> + Home +

React Render Perf

+
    +
  • + + 01 - Memoizing Expensive Components + +
  • +
  • + + 02 - Prevent Context From Rendering + +
  • +
  • + + 03 - Avoid Using Context + +
  • +
  • + + 04 - Splitting Up Large Components + +
  • +
+ + ); +} diff --git a/src/routes.tsx b/src/routes.tsx index 53ff88c..72ad9ae 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,6 +1,6 @@ import {BrowserRouter, Route} from "react-router-dom"; -import ReactRenderPerf from "./react-render-perf"; +import ReactRenderPerfRoutes from "./react-render-perf/routes"; import Homepage from "./homepage"; export default function Routes() { @@ -9,8 +9,8 @@ export default function Routes() { - - + + );