From 5a81b9a10f8b5774b9bc26e29a822df5da9eb9b0 Mon Sep 17 00:00:00 2001 From: Kevin Barabash Date: Wed, 3 Jul 2024 14:30:28 -0400 Subject: [PATCH 1/2] Revise Lesson 2 --- src/react-render-perf/content.mdx | 4 +- src/react-render-perf/lesson-01/index.tsx | 3 + src/react-render-perf/lesson-02/content.mdx | 79 ++++++++++++------- .../lesson-02/exercise/index.tsx | 11 +++ src/react-render-perf/lesson-02/index.tsx | 3 + src/react-render-perf/lesson-03/index.tsx | 3 + src/react-render-perf/lesson-04/index.tsx | 3 + 7 files changed, 77 insertions(+), 29 deletions(-) diff --git a/src/react-render-perf/content.mdx b/src/react-render-perf/content.mdx index 213a7eb..31ae0c6 100644 --- a/src/react-render-perf/content.mdx +++ b/src/react-render-perf/content.mdx @@ -6,8 +6,8 @@ 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. +The purpose of this workshop is to learn about common performance issues that can +result from react renders taking longer than expected. ## Preliminaries diff --git a/src/react-render-perf/lesson-01/index.tsx b/src/react-render-perf/lesson-01/index.tsx index 519f44a..02374d5 100644 --- a/src/react-render-perf/lesson-01/index.tsx +++ b/src/react-render-perf/lesson-01/index.tsx @@ -19,6 +19,9 @@ export default function Page() {
+

+ ⚠️ HMR may not work, please reload after making changes ⚠️ +

diff --git a/src/react-render-perf/lesson-02/content.mdx b/src/react-render-perf/lesson-02/content.mdx index 6c17337..ee7fa10 100644 --- a/src/react-render-perf/lesson-02/content.mdx +++ b/src/react-render-perf/lesson-02/content.mdx @@ -1,7 +1,8 @@ -React Context is useful for sharing data with its descendent components. When -changes are made to the context, this causes the context and all of its -descendents to be re-rendered. If the number of descendents is large, this can -result in poor rendering performance. +React Context is useful for sharing data with its descendent components without +having to perform prop-drilling (passing props down through several layers of +components). When changes are made to the context, this causes the context and all +of its descendents to be re-rendered. If the number of descendents is large, this +can result in poor rendering performance. In order to avoid re-rendering all of the descendents we'd like only those components using the context to re-render. Ideally, they should only re-render @@ -12,27 +13,27 @@ re-render both child components of the context even though only one of them needs to be updated. ```tsx filename="index.tsx" -import {createContext, useContext, useState} from "react"; +import * as React from "react"; type FooBar = { foo: number; bar: number; }; -const FooBarContext = createContext({foo: 0, bar: 0}); +const FooBarContext = React.createContext({foo: 0, bar: 0}); const Foo = () => { - const {foo} = useContext(FooBarContext); + const {foo} = React.useContext(FooBarContext); return

foo = {foo}

; }; const Bar = () => { - const {bar} = useContext(FooBarContext); + const {bar} = React.useContext(FooBarContext); return

bar = {bar}

; }; const Parent = () => { - const [value, setValue] = useState({foo: 0, bar: 0}); + const [value, setValue] = React.useState({foo: 0, bar: 0}); const incrementFoo = () => setValue(fb => {...fb, foo: fb.foo + 1}); const incrementBar = () => setValue(fb => {...fb, bar: fb.bar + 1}); <> @@ -56,31 +57,37 @@ data they care about. The parent component will emit the appropriate event when increment the values for foo and bar. ```tsx -import {createContext, useContext, useState, useMemo} from "react"; +import * as React from "react"; import EventEmitter from "eventemitter3"; -const FooBarContext = createContext(null); +const FooBarContext = React.createContext(null); const Foo = () => { - const emitter = useContext(FooBarContext); - const [foo, setFoo] = useState(0); - emitter?.on("foo", setFoo); + const emitter = React.useContext(FooBarContext); + const [foo, setFoo] = React.useState(0); + React.useEffect(() => { + emitter?.on("foo", setFoo); + return () => emitter?.off("foo", setFoo); + }, []); return

foo = {foo}

; }; const Bar = () => { - const emitter = useContext(FooBarContext); - const [bar, setBar] = useState(0); - emitter?.on("bar", setBar); + const emitter = React.useContext(FooBarContext); + const [bar, setBar] = React.useState(0); + React.useEffect(() => { + emitter?.on("bar", setBar); + return () => emitter?.off("bar", setBar); + }); return

bar = {bar}

; }; const Parent = () => { - const [foo, setFoo] = useState(0); - const [bar, setBar] = useState(0); - const emitter = useMemo(() => new EventEmitter(), []); + const [foo, setFoo] = React.useState(0); + const [bar, setBar] = React.useState(0); + const emitter = React.useMemo(() => new EventEmitter(), []); const incrementFoo = () => { emitter.emit("foo", foo + 1); @@ -109,26 +116,44 @@ need to memoize Foo and Bar themselves. Thankfully the props so this is trivial to do. ```tsx -import {createContext, useContext, useState, memo} from "react"; +import * as React from "react"; import arePropsEqual from "react-fast-compare"; -const Foo = memo(() => { - const emitter = useContext(FooBarContext); - const [foo, setFoo] = useState(0); +const Foo = React.memo(() => { + const emitter = React.useContext(FooBarContext); + const [foo, setFoo] = React.useState(0); emitter?.on("foo", setFoo); return

foo = {foo}

; }, arePropsEqual); -const Bar = memo(() => { - const emitter = useContext(FooBarContext); - const [bar, setBar] = useState(0); +const Bar = React.memo(() => { + const emitter = React.useContext(FooBarContext); + const [bar, setBar] = React.useState(0); emitter?.on("bar", setBar); return

bar = {bar}

; }, arePropsEqual); ``` +## Notes + +- The example above shows the Parent component creating the event emitter. This is + only necessary if Parent was being used in multiple places and we didn't want its + descendents sharing the same event emitter. If there's only a single instance of + Parent the event emitter could be created outside of the component and could be + used to initialize the context. This would result in the context value always being + defined and we wouldn't need to use optional chaining when accessing properties on + the emitter we get from the context. +- Why not just use redux for prop-drilling? Unfortunately, it has the same issues + as the naive use of context shown in the original code above. That being said, the context + + event emitter pattern isn't great either because it's a pattern as opposed to a library. In + the long term I think we should evaluate possible replaces for redux that don't + have this preformance issue like jotai, zustand, or recoil.js. +- React.useMemo(callback) and React.memo(Component, arePropsEqual?) are + both use to memoize things. The former is used for memoizing computations _within_ a functional + component while the latter is used for memoizing functional components themselves. + ## Exercise 1. Use the profiler in React dev tools to measure the render performance of the diff --git a/src/react-render-perf/lesson-02/exercise/index.tsx b/src/react-render-perf/lesson-02/exercise/index.tsx index 1b75136..241dac7 100644 --- a/src/react-render-perf/lesson-02/exercise/index.tsx +++ b/src/react-render-perf/lesson-02/exercise/index.tsx @@ -4,6 +4,17 @@ export default function Exercise2() { return (

Exercise 2: Prevent Context From Rendering

+

+ Below is a colour picker. Clicking on it will select the colour + under the mouse cursor. Profile to see where the performance + issues are and then resolve them using the technique described + in this lesson. +

+

+ NOTE: There are multiple components in this example that could + be memoized. Part of this exercise is figuring out which ones + make sense to memoize. +

); diff --git a/src/react-render-perf/lesson-02/index.tsx b/src/react-render-perf/lesson-02/index.tsx index 7a03394..960d559 100644 --- a/src/react-render-perf/lesson-02/index.tsx +++ b/src/react-render-perf/lesson-02/index.tsx @@ -19,6 +19,9 @@ export default function Page() {
+

+ ⚠️ HMR may not work, please reload after making changes ⚠️ +

diff --git a/src/react-render-perf/lesson-03/index.tsx b/src/react-render-perf/lesson-03/index.tsx index 073c7b6..6ac17f2 100644 --- a/src/react-render-perf/lesson-03/index.tsx +++ b/src/react-render-perf/lesson-03/index.tsx @@ -19,6 +19,9 @@ export default function Page() {
+

+ ⚠️ HMR may not work, please reload after making changes ⚠️ +

diff --git a/src/react-render-perf/lesson-04/index.tsx b/src/react-render-perf/lesson-04/index.tsx index c43c8a0..b1c9bab 100644 --- a/src/react-render-perf/lesson-04/index.tsx +++ b/src/react-render-perf/lesson-04/index.tsx @@ -19,6 +19,9 @@ export default function Page() {
+

+ ⚠️ HMR may not work, please reload after making changes ⚠️ +

From e5a43dcd79ca9c44965abdade405d46a37aa1402 Mon Sep 17 00:00:00 2001 From: Kevin Barabash Date: Thu, 4 Jul 2024 17:52:08 -0400 Subject: [PATCH 2/2] fix typo --- src/react-render-perf/lesson-02/content.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-render-perf/lesson-02/content.mdx b/src/react-render-perf/lesson-02/content.mdx index ee7fa10..6cbf66f 100644 --- a/src/react-render-perf/lesson-02/content.mdx +++ b/src/react-render-perf/lesson-02/content.mdx @@ -151,7 +151,7 @@ const Bar = React.memo(() => { the long term I think we should evaluate possible replaces for redux that don't have this preformance issue like jotai, zustand, or recoil.js. - React.useMemo(callback) and React.memo(Component, arePropsEqual?) are - both use to memoize things. The former is used for memoizing computations _within_ a functional + both used to memoize things. The former is used for memoizing computations _within_ a functional component while the latter is used for memoizing functional components themselves. ## Exercise