From 2b36dc1e7fd698e72e89191408d15da363cb2df5 Mon Sep 17 00:00:00 2001 From: Thomas Hintz Date: Tue, 15 Sep 2020 08:58:02 -0700 Subject: [PATCH] Updates. --- code/hp-react.js | 35 +++ high-performance-react.org | 279 +++++++++++++++++- manuscript/code-splitting.markua | 2 + manuscript/diagnosing-bottlenecks.markua | 36 ++- ...undamentals--building-our-own-react.markua | 2 +- manuscript/rendering-model.markua | 128 +++++++- 6 files changed, 460 insertions(+), 22 deletions(-) diff --git a/code/hp-react.js b/code/hp-react.js index 7ccf616..34988ca 100644 --- a/code/hp-react.js +++ b/code/hp-react.js @@ -140,7 +140,42 @@ function renderChildren(element, domElement, prevElement = { props: { children: }); } +function defaultAreEqual(oldProps, newProps) { + if (typeof oldProps !== 'object' || typeof newProps !== 'object') { + return false; + } + + const oldKeys = Object.keys(oldProps); + const newKeys = Object.keys(newProps); + + if (oldKeys.length !== newKeys.length) { + return false; + } + + for (let i = 0; i < oldKeys.length; i++) { + // Object.is - the comparison to note + if (!oldProps.hasOwnProperty(newKeys[i]) || + !Object.is(oldProps[newKeys[i]], newProps[newKeys[i]])) { + return false; + } + } + + return true; +} +function memo(component, areEqual = defaultAreEqual) { + let oldProps = {}; + let lastResult = false; + return (props) => { + if (lastResult && areEqual(oldProps, props) { + return lastResult; + } else { + lastResult = component(props); + oldProps = Object.assign({}, props); // shallow copy + return lastResult; + } + }; +} var Hello = memo(({ dateTime }) => { return ['h1', {}, [`It is: ${dateTime}`]]; diff --git a/high-performance-react.org b/high-performance-react.org index d727b74..e85004e 100644 --- a/high-performance-react.org +++ b/high-performance-react.org @@ -793,32 +793,32 @@ discouraged for class based components. So if we have a large tree of components and we change state high in the tree React will be constantly re-rendering large parts of the tree. (This is common because app state often has to live up high in the tree because props -can only be passed down.) This is clearly very in efficient so why +can only be passed down.) This is clearly very inefficient so why does React do it? If you remember back to when we implemented the render algorithm you'll recall that React does nothing to see if a component actually -needs to re-render, it only cares tests whether DOM elements need to +needs to re-render, it only tests whether DOM elements need to be replaced or removed. Instead React always renders all children. React is effectively off-loading the descision to re-render to the components themselves because a general solution has poor performance. Originally React had ~shouldComponentUpdate~ to solve this issue but -the developers of React found that implementing it correctly was -difficult and error prone. Programmers would add new props to a +the developers of React found that for users implementing it correctly +was difficult and error prone. Programmers would add new props to a component but forget to update ~shouldComponentUpdate~ with the new props causing the component to not update when it should which led to strange and hard to diagnose bugs. So if we shouldn't use ~shouldComponentUpdate~ what tools are we left with? And it's a great question because unneeded renders can be a massive -bottleneck. Especially on large lists of components. In fact, there is +bottleneck, especially on large lists of components. In fact, there is no other way to control renders; React will always render. But there is still hope. While we can't control if our component will -render what if instead of just always -rerunning all of our render -code on each render we instead kept a copy of the result of the render +render, what if instead of just always re-running all of our render +code on each render, we instead kept a copy of the result of the render and next time React asks us to re-render we just return the result we saved? Now that, with two modifications, is exactly what we will do. @@ -846,9 +846,21 @@ and analyze when and how to use it. ** ~React.memo~ The first API React provides that we will look at is -~React.memo~. ~React.memo~ is a higher-order component -(HOC) that wraps your functional component. It handles memoizing your -component based on its props (not state). +~React.memo~. ~React.memo~ is a higher-order component (HOC) that +wraps your functional component. It handles partially memoizing your +component based on its props (not state). If your component contains +~useState~ or ~useContext~ hooks it will re-render when the state or +context changes. + +It is important to note that while React named their function "memo" +it is more like a partial memoization compared to the usual definition +of memoization. Normally in memoization when a function is given the +same inputs as a previous invocation it will just return a stored +result, however, with React's ~memo~ only the /last/ invocation is +memoized. So if you have a prop that alternates between two different +values React's ~memo~ will always re-render the component whereas with +traditional memoization the component would only ever get rendered +twice in total. Here is the signature for ~React.memo~: @@ -864,7 +876,7 @@ component will produce the same output. If the second argument is not specified then React performs a /shallow/ comparison between props it has received in the past and the current props. If the current props match props that have been passed -to your component before React will use the output stored from that +to your component before, React will use the output stored from that previous render instead of rendering your component again. If you want more control over the prop comparison, like if you wanted to deeply compare some props, you would pass in your own ~areEqual?~. However, @@ -876,12 +888,253 @@ using ~areEqual?~ because it can suffer from the same problem that ** ~React.PureComponent~ -TODO useCallback +~React.PureComponent~ is very similar to ~React.memo~, but for class +based components. Like ~React.memo~, ~React.PureComponent~ memoizes +the component based on a shallow comparison of its props and state. + +Here is the signature for ~React.PureComponent~: + +#+BEGIN_SRC javascript +class Pancake extends React.PureComponent { + ... +} +#+END_SRC + +** Adding support for memoization to our React + +Implementing full-blown memoization would be outside the scope of this +book but since React only memoizes the last render it is quite easy +for us to add ~memo~ support. + +The most interesting part of the ~memo~ implementation is the default +~areEqual~ implementation. This is the implementation components will +use if they don't provide their own. To see if ~memo~ can return a +previous render or not it compares the props to see if they are the +same use the following ~defaultAreEqual~ function. This what that +looks like: + +#+BEGIN_SRC javascript +function defaultAreEqual(oldProps, newProps) { + if (typeof oldProps !== 'object' || typeof newProps !== 'object') { + return false; + } + + const oldKeys = Object.keys(oldProps); + const newKeys = Object.keys(newProps); + + if (oldKeys.length !== newKeys.length) { + return false; + } + + for (let i = 0; i < oldKeys.length; i++) { + // Object.is - the comparison to note + if (!oldProps.hasOwnProperty(newKeys[i]) || + !Object.is(oldProps[newKeys[i]], newProps[newKeys[i]])) { + return false; + } + } + + return true; +} +#+END_SRC + +~oldProps~ and ~newProps~ are objects containing the previous render's +props and the current render's props. Much of the function is just +boilerplate to ensure the prop objects are the same type and +shape. The important part is noted in the loop where we use +JavaScript's ~Object.is~ method to compare each prop object's +values. + +#+begin_note +If you're not familiar with ~Object.is~, it is nearly the same as the +identity operator ~===~ except it treats ~-0~ and ~+0~ as equal but +not does not treat ~Number.NaN~ as equal to ~NaN~. +#+end_note + +It is important to notice that if a prop value is an object then we +are /not/ testing its contents, only whether the objects themselves +are the same object or not. For example, if we have two props ~a~ and +~b~ set to the following objects that look the same they will cause +~defaultAreEqual~ to return false. + +#+BEGIN_SRC javascript +const a = { x: 1 }; +const b = { x: 1 }; +Object.is(a, b); // false +#+END_SRC + +Even though ~a~ and ~b~ look like the same object they are in fact +instances of two different objects and will therefore cause ~memo~ to +not find a match and your component will re-render. Using object +literals, like in the example above, as prop values is a very common +pattern that will "break" memoization of a component. + +This is also a potential pitfall in another way: + +#+BEGIN_SRC javascript +const a = { x: 1 }; +const b = a; +Object.is(a, b); // true +a.x = 2; +Object.is(a, b); // true +#+END_SRC + +In this example it may be obvious that ~a~ still equals ~b~ at the end +but in React applications this is often less clear because the object +being used as a prop is coming from somewhere else. The lesson to +watch out for is that if you pass the same object to a memoized +component while changing that object's contents between renders the +memoized component won't know that the contents have changed and will +instead return a cached render instead of doing what you probably are +expecting: re-rendering. So the overall lesson when using objects in +props with memoized components is that objects with the same contents +should be the same object and objects with different contents should +be different objects. If managing this is a problem in your +application there are immutability libraries that you can use that can +help out. + +As you can see there is a cost to memoizing a component both in +computer resources and programmer effort so it is important to only +apply memoization when a component needs it and will benefit from it. + +If a component only renders a few times or infrequently it is not a +good candidate for memoization since it is unlikely that a memoized +render result will get returned and even if it does it is unlikely to +make up for the cost of implementing and using it unless its rendering +process is unusually computationally intense. + +Another case when memoization is not a good idea is when the props for +a component are not often the same as a previous render. Like take, +for example, a component that renders the current hours, minutes, and +seconds and receives those inputs as props. Unless you're rendering +that component multiple times per second the props will never be the +same as a previous render. So if you were to memoize that component +you would be using CPU cycles for the memoization process and filling +up memory with render results without ever being able to re-use a +render. + +Here some rules for working with memoized components: + +- Don't use object literals +- Don't modify objects +- Objects with the same contents should be the same instance +- Use memoization: on components that get called frequently +- Use memoization: when props will often be the same for multiple + renders in succession +- Use memoization: to prevent part of the component tree from + re-rendering + +And finally we have the ~memo~ implementation: -* Diagnosing Bottlenecks +#+BEGIN_SRC javascript +function memo(component, areEqual = defaultAreEqual) { + let oldProps = []; + let lastResult = false; + return (props) => { + const newProps = propsToArray(props); + if (lastResult && areEqual(oldProps, newProps) { + return lastResult; + } else { + lastResult = component(props); + oldProps = newProps; + return lastResult; + } + }; +} +#+END_SRC + +~memo~ is quite straightforward. We just store the previous props and +result and if the new props match the old props we return the last +result. In React this is also connected to the ~useState~ and +~useContext~ hooks so that whenever state is changed a re-render is +forced and the result stored. + +Of course, you can provide your own ~areEqual~ implementation instead +of using the default shallow comparison version. When might this make +sense and are there any performance considerations in doing so? + +The default shallow comparison method is relatively fast so by itself +it is unlikely to be a performance bottleneck so the only reason to +implement your own version is if you want ~areEqual~ to do a deeper +comparison of the props, like comparing the contents of objects passed +as props or the contents of arrays. You could just write your own +implementation that does more involved comparisons on the props that +you want it to but that is also a potential pitfall. Like if another +developer adds a new prop to the component but doesn't realize there +is a custom ~areEqual~ implementation the component will break since +it won't detect when the new prop has a new value and therefore won't +trigger a new render. A better approach is to use a generic deep +comparison procedure that does a deep comparison on all props but this +can easily become a performance bottleneck so use it with care (and is +likely the reason React doesn't use it by default). + +TODO useCallback +* Identifying & Diagnosing Bottlenecks :PROPERTIES: :EXPORT_FILE_NAME: manuscript/diagnosing-bottlenecks.markua :END: + +When trying to improve performance the most important and value step +in the process is identifying and diagnosing bottlenecks. You can +attempt to guess at how to write high-performance code from the start, +and to a small extent that can help, but with the complexity of the +web ecosystem today that is very unlikely to be sufficient. There are +just too many layers and components in the system to always get it +right. So inevitably you will run in to performance bottlenecks. + +There are six main steps to solving performance bottlenecks, with +some parts done in a cycle: + +- Describing the performance issue +- Measuring the issue +- Identifying the source of the problem +- Diagnosing the cause +- Generating possible solutions +- Selecting and implementing a solution +- Measuring the issue again + +** Describing Performance Issues + +The first step is to identify and qualify the bottleneck. You can +start by asking "what are the symptoms?" and "what triggers +them?". It's very important to dig in as much as possible at this +stage and gather as much information as possible. Here are the +questions I generally use to get you started: + +- What specifically is the issue? + - A lack of responsiveness? + - Temporary jankiness? + - What am I/the user being prevented from doing? +- When does the issue start? +- When does it finish? +- Is the intensity and/or duration variable or constant? +- Is it predictable? Does it always happen? +- Does it seem like anything triggers it? + +You can start with trying to answer these questions or you can come up +with your own. The only way to get good at it is practice as it's more +and an art than a science at this stage. + +This stage might not seem that important or the answers might seem +obvious but being thorough here can actually save you a lot of time +later on. It's very easy to misunderstand a bottleneck and then begin +your investigation in the wrong place, wasting valuable time, or even +worse, crafting a solution to the wrong problem. + +** Measuring + +Once you've described the performance issue in as much detail as you +can it's time to move on to the next stage: measuring the issue. This +stage is vital to the process. Do not skip this stage. The only way +later on to ensure you've actually fixed the problem is to measure the +problem to the best of your ability. The better you can quantify the +issue the easier the rest of the process will be. This can be very +challenging, especially with things that seem immeasurable, like UI +jankiness but if you work at it there is generally a way to do. We +will discuss a few techniques coming up. + + + * Reducing Renders :PROPERTIES: :EXPORT_FILE_NAME: manuscript/reducing-renders.markua diff --git a/manuscript/code-splitting.markua b/manuscript/code-splitting.markua index 3dd102c..a1f65b3 100644 --- a/manuscript/code-splitting.markua +++ b/manuscript/code-splitting.markua @@ -4,4 +4,6 @@ React.lazy, suspense use on routes +how to handle updates of assets that have new names? + diff --git a/manuscript/diagnosing-bottlenecks.markua b/manuscript/diagnosing-bottlenecks.markua index 5d08a66..0669514 100644 --- a/manuscript/diagnosing-bottlenecks.markua +++ b/manuscript/diagnosing-bottlenecks.markua @@ -1,3 +1,37 @@ -# Diagnosing Bottlenecks +# Identifying & Diagnosing Bottlenecks + +When trying to improve performance the most important and value step in the process is identifying and diagnosing bottlenecks. You can attempt to guess at how to write high-performance code from the start, and to a small extent that can help, but with the complexity of the web ecosystem today that is very unlikely to be sufficient. There are just too many layers and components in the system to always get it right. So inevitably you will run in to performance bottlenecks. + +There are six main steps to solving performance bottlenecks, with some parts done in a cycle: + +* Describing the performance issue +* Measuring the issue +* Identifying the source of the problem +* Diagnosing the cause +* Generating possible solutions +* Selecting and implementing a solution +* Measuring the issue again + +## Describing Performance Issues + +The first step is to identify and qualify the bottleneck. You can start by asking "what are the symptoms?" and "what triggers them?". It's very important to dig in as much as possible at this stage and gather as much information as possible. Here are the questions I generally use to get you started: + +* What specifically is the issue? + * A lack of responsiveness? + * Temporary jankiness? + * What am I/the user being prevented from doing? +* When does the issue start? +* When does it finish? +* Is the intensity and/or duration variable or constant? +* Is it predictable? Does it always happen? +* Does it seem like anything triggers it? + +You can start with trying to answer these questions or you can come up with your own. The only way to get good at it is practice as it's more and an art than a science at this stage. + +This stage might not seem that important or the answers might seem obvious but being thorough here can actually save you a lot of time later on. It's very easy to misunderstand a bottleneck and then begin your investigation in the wrong place, wasting valuable time, or even worse, crafting a solution to the wrong problem. + +## Measuring + +Once you've described the performance issue in as much detail as you can it's time to move on to the next stage: measuring the issue. This stage is vital to the process. Do not skip this stage. The only way later on to ensure you've actually fixed the problem is to measure the problem to the best of your ability. The better you can quantify the issue the easier the rest of the process will be. This can be very challenging, especially with things that seem immeasurable, like UI jankiness but if you work at it there is generally a way to do. We will discuss a few techniques coming up. diff --git a/manuscript/fundamentals--building-our-own-react.markua b/manuscript/fundamentals--building-our-own-react.markua index 4830aa6..1a51345 100644 --- a/manuscript/fundamentals--building-our-own-react.markua +++ b/manuscript/fundamentals--building-our-own-react.markua @@ -1,4 +1,4 @@ -# Fundamentals: Building our own React +# Foundations: Building our own React Baking bread. When I first began to learn how to bake bread the recipe told me what to do. It listed some ingredients and told me how to combine them and prescribed times of rest. It gave me an oven temperature and a period of wait. It gave me mediocre bread of wildly varying quality. I tried different recipes but the result was always the same. diff --git a/manuscript/rendering-model.markua b/manuscript/rendering-model.markua index 1189a8b..6f20522 100644 --- a/manuscript/rendering-model.markua +++ b/manuscript/rendering-model.markua @@ -6,15 +6,15 @@ TODO insert img-tree of components In figure 1, if state changes in component A but nothing changes in B will React ask B to re-render? -Yes. Absolutely. Always, unless `shouldComponentUpdate` returns false, which is not even an option with functional components and is discouraged for class based components. So if we have a large tree of components and we change state high in the tree React will be constantly re-rendering large parts of the tree. (This is common because app state often has to live up high in the tree because props can only be passed down.) This is clearly very in efficient so why does React do it? +Yes. Absolutely. Always, unless `shouldComponentUpdate` returns false, which is not even an option with functional components and is discouraged for class based components. So if we have a large tree of components and we change state high in the tree React will be constantly re-rendering large parts of the tree. (This is common because app state often has to live up high in the tree because props can only be passed down.) This is clearly very inefficient so why does React do it? -If you remember back to when we implemented the render algorithm you'll recall that React does nothing to see if a component actually needs to re-render, it only cares tests whether DOM elements need to be replaced or removed. Instead React always renders all children. React is effectively off-loading the descision to re-render to the components themselves because a general solution has poor performance. +If you remember back to when we implemented the render algorithm you'll recall that React does nothing to see if a component actually needs to re-render, it only tests whether DOM elements need to be replaced or removed. Instead React always renders all children. React is effectively off-loading the descision to re-render to the components themselves because a general solution has poor performance. -Originally React had `shouldComponentUpdate` to solve this issue but the developers of React found that implementing it correctly was difficult and error prone. Programmers would add new props to a component but forget to update `shouldComponentUpdate` with the new props causing the component to not update when it should which led to strange and hard to diagnose bugs. So if we shouldn't use `shouldComponentUpdate` what tools are we left with? +Originally React had `shouldComponentUpdate` to solve this issue but the developers of React found that for users implementing it correctly was difficult and error prone. Programmers would add new props to a component but forget to update `shouldComponentUpdate` with the new props causing the component to not update when it should which led to strange and hard to diagnose bugs. So if we shouldn't use `shouldComponentUpdate` what tools are we left with? -And it's a great question because unneeded renders can be a massive bottleneck. Especially on large lists of components. In fact, there is no other way to control renders; React will always render. +And it's a great question because unneeded renders can be a massive bottleneck, especially on large lists of components. In fact, there is no other way to control renders; React will always render. -But there is still hope. While we can't control if our component will render what if instead of just always -rerunning all of our render code on each render we instead kept a copy of the result of the render and next time React asks us to re-render we just return the result we saved? Now that, with two modifications, is exactly what we will do. +But there is still hope. While we can't control if our component will render, what if instead of just always re-running all of our render code on each render, we instead kept a copy of the result of the render and next time React asks us to re-render we just return the result we saved? Now that, with two modifications, is exactly what we will do. TODO Note: this stops full tree from re-rendering @@ -30,7 +30,9 @@ We will learn about this API by first looking at the signatures of the React API ## `React.memo` -The first API React provides that we will look at is `React.memo`. `React.memo` is a higher-order component (HOC) that wraps your functional component. It handles memoizing your component based on its props (not state). +The first API React provides that we will look at is `React.memo`. `React.memo` is a higher-order component (HOC) that wraps your functional component. It handles partially memoizing your component based on its props (not state). If your component contains `useState` or `useContext` hooks it will re-render when the state or context changes. + +It is important to note that while React named their function "memo" it is more like a partial memoization compared to the usual definition of memoization. Normally in memoization when a function is given the same inputs as a previous invocation it will just return a stored result, however, with React's `memo` only the *last* invocation is memoized. So if you have a prop that alternates between two different values React's `memo` will always re-render the component whereas with traditional memoization the component would only ever get rendered twice in total. Here is the signature for `React.memo`: @@ -41,10 +43,122 @@ function (Component, areEqual?) { ... } It takes two arguments, one required and one optional. The required argument is the component you want to memoize. The second and optional argument is a function that allows you to tell React when your component will produce the same output. -If the second argument is not specified then React performs a *shallow* comparison between props it has received in the past and the current props. If the current props match props that have been passed to your component before React will use the output stored from that previous render instead of rendering your component again. If you want more control over the prop comparison, like if you wanted to deeply compare some props, you would pass in your own `areEqual?`. However, it's generally recommended to program in a more pure style instead of using `areEqual?` because it can suffer from the same problem that `shouldComponentUpdate` did. +If the second argument is not specified then React performs a *shallow* comparison between props it has received in the past and the current props. If the current props match props that have been passed to your component before, React will use the output stored from that previous render instead of rendering your component again. If you want more control over the prop comparison, like if you wanted to deeply compare some props, you would pass in your own `areEqual?`. However, it's generally recommended to program in a more pure style instead of using `areEqual?` because it can suffer from the same problem that `shouldComponentUpdate` did. ## `React.PureComponent` +`React.PureComponent` is very similar to `React.memo`, but for class based components. Like `React.memo`, `React.PureComponent` memoizes the component based on a shallow comparison of its props and state. + +Here is the signature for `React.PureComponent`: + +{format: "javascript"} +``` +class Pancake extends React.PureComponent { + ... +} +``` + +## Adding support for memoization to our React + +Implementing full-blown memoization would be outside the scope of this book but since React only memoizes the last render it is quite easy for us to add `memo` support. + +The most interesting part of the `memo` implementation is the default `areEqual` implementation. This is the implementation components will use if they don't provide their own. To see if `memo` can return a previous render or not it compares the props to see if they are the same use the following `defaultAreEqual` function. This what that looks like: + +{format: "javascript"} +``` +function defaultAreEqual(oldProps, newProps) { + if (typeof oldProps !== 'object' || typeof newProps !== 'object') { + return false; + } + + const oldKeys = Object.keys(oldProps); + const newKeys = Object.keys(newProps); + + if (oldKeys.length !== newKeys.length) { + return false; + } + + for (let i = 0; i < oldKeys.length; i++) { + // Object.is - the comparison to note + if (!oldProps.hasOwnProperty(newKeys[i]) || + !Object.is(oldProps[newKeys[i]], newProps[newKeys[i]])) { + return false; + } + } + + return true; +} +``` + +`oldProps` and `newProps` are objects containing the previous render's props and the current render's props. Much of the function is just boilerplate to ensure the prop objects are the same type and shape. The important part is noted in the loop where we use JavaScript's `Object.is` method to compare each prop object's values. + +I> If you're not familiar with `Object.is`, it is nearly the same as the identity operator `===` except it treats `-0` and `+0` as equal but not does not treat `Number.NaN` as equal to `NaN`. + +It is important to notice that if a prop value is an object then we are *not* testing its contents, only whether the objects themselves are the same object or not. For example, if we have two props `a` and `b` set to the following objects that look the same they will cause `defaultAreEqual` to return false. + +{format: "javascript"} +``` +const a = { x: 1 }; +const b = { x: 1 }; +Object.is(a, b); // false +``` + +Even though `a` and `b` look like the same object they are in fact instances of two different objects and will therefore cause `memo` to not find a match and your component will re-render. Using object literals, like in the example above, as prop values is a very common pattern that will "break" memoization of a component. + +This is also a potential pitfall in another way: + +{format: "javascript"} +``` +const a = { x: 1 }; +const b = a; +Object.is(a, b); // true +a.x = 2; +Object.is(a, b); // true +``` + +In this example it may be obvious that `a` still equals `b` at the end but in React applications this is often less clear because the object being used as a prop is coming from somewhere else. The lesson to watch out for is that if you pass the same object to a memoized component while changing that object's contents between renders the memoized component won't know that the contents have changed and will instead return a cached render instead of doing what you probably are expecting: re-rendering. So the overall lesson when using objects in props with memoized components is that objects with the same contents should be the same object and objects with different contents should be different objects. If managing this is a problem in your application there are immutability libraries that you can use that can help out. + +As you can see there is a cost to memoizing a component both in computer resources and programmer effort so it is important to only apply memoization when a component needs it and will benefit from it. + +If a component only renders a few times or infrequently it is not a good candidate for memoization since it is unlikely that a memoized render result will get returned and even if it does it is unlikely to make up for the cost of implementing and using it unless its rendering process is unusually computationally intense. + +Another case when memoization is not a good idea is when the props for a component are not often the same as a previous render. Like take, for example, a component that renders the current hours, minutes, and seconds and receives those inputs as props. Unless you're rendering that component multiple times per second the props will never be the same as a previous render. So if you were to memoize that component you would be using CPU cycles for the memoization process and filling up memory with render results without ever being able to re-use a render. + +Here some rules for working with memoized components: + +* Don't use object literals +* Don't modify objects +* Objects with the same contents should be the same instance +* Use memoization: on components that get called frequently +* Use memoization: when props will often be the same for multiple renders in succession +* Use memoization: to prevent part of the component tree from re-rendering + +And finally we have the `memo` implementation: + +{format: "javascript"} +``` +function memo(component, areEqual = defaultAreEqual) { + let oldProps = []; + let lastResult = false; + return (props) => { + const newProps = propsToArray(props); + if (lastResult && areEqual(oldProps, newProps) { + return lastResult; + } else { + lastResult = component(props); + oldProps = newProps; + return lastResult; + } + }; +} +``` + +`memo` is quite straightforward. We just store the previous props and result and if the new props match the old props we return the last result. In React this is also connected to the `useState` and `useContext` hooks so that whenever state is changed a re-render is forced and the result stored. + +Of course, you can provide your own `areEqual` implementation instead of using the default shallow comparison version. When might this make sense and are there any performance considerations in doing so? + +The default shallow comparison method is relatively fast so by itself it is unlikely to be a performance bottleneck so the only reason to implement your own version is if you want `areEqual` to do a deeper comparison of the props, like comparing the contents of objects passed as props or the contents of arrays. You could just write your own implementation that does more involved comparisons on the props that you want it to but that is also a potential pitfall. Like if another developer adds a new prop to the component but doesn't realize there is a custom `areEqual` implementation the component will break since it won't detect when the new prop has a new value and therefore won't trigger a new render. A better approach is to use a generic deep comparison procedure that does a deep comparison on all props but this can easily become a performance bottleneck so use it with care (and is likely the reason React doesn't use it by default). + TODO useCallback