From bde43fe508cb93778852397e871e28a8e103195a Mon Sep 17 00:00:00 2001 From: Thomas Hintz Date: Sun, 26 Jul 2020 10:37:14 -0700 Subject: [PATCH] Expanding first chapter. --- .gitignore | 1 + high-performance-react.org | 124 ++++++++++++++++-- ...undamentals--building-our-own-react.markua | 36 +++-- 3 files changed, 141 insertions(+), 20 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4e5f6c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ \ No newline at end of file diff --git a/high-performance-react.org b/high-performance-react.org index fd2d8c6..bcc84eb 100644 --- a/high-performance-react.org +++ b/high-performance-react.org @@ -454,10 +454,16 @@ Now, to get started with our render method we must make some modifications to our previous render methods. First, we need to be able to store and retrieve the previous render tree. Then we need to add code to compare parts of the tree to decide if we need to -re-render something or if we can re-use it from the previous render -tree. - -Here we are adding a global object that will store our last render +re-render something or if we can re-use DOM elements from the previous +render tree. And last we need to return a tree of elements that can be +used in the next render as a comparison and to reference the DOM +elements that we create. These new elements will have the same +structure as our current elements but we will add two new properties: +~domElement~ and ~parent~. ~domElement~ is the DOM element associated +with our synthetic element and ~parent~ is a reference to the parent +DOM element. + +Here we begin by adding a global object that will store our last render tree, keyed by the ~container~. #+BEGIN_SRC javascript @@ -470,10 +476,17 @@ function render(element, container) { } #+END_SRC -TODO note that we are adding parent and domElement properties. - -Now that we have a way to see what we rendered last time we can go -ahead and update our render method with the heuristics. +Now that we have stored our last render tree we can go ahead and +update our render method with the heuristics for reusing the DOM +elements. We name it ~render_internal~ because it is what controls the +rendering but takes an additional argument now: the +~prevElement~. ~preElement~ is a reference to the corresponding +~element~ from the previous render and contains a reference to its +associated DOM element and parent DOM element. If it's the first +render or if we are rendering a new node or branch to the tree than +~prevElement~ will be ~undefined~. If, however, ~element~ is +~undefined~ and ~prevElement~ is defined then we know we need to +delete a node that previously existed. #+BEGIN_SRC javascript function render_internal(element, container, prevElement) { @@ -485,7 +498,7 @@ function render_internal(element, container, prevElement) { domElement = createDOMElement(element); } else if (element.type === prevElement.type) { domElement = prevElement.domElement; - } else { + } else { // types don't match removeDOMElement(prevElement); domElement = createDOMElement(element); } @@ -508,12 +521,38 @@ function render_internal(element, container, prevElement) { } #+END_SRC +The only time we can't set DOM properties on our element and render +its children is when we are deleting an existing DOM element. We use +this observation to group the calls for ~setDOMProps~ and +~renderChildren~. Choosing when to append a new DOM element to the +container is also part of the heuristics. If we can reuse an existing +DOM element then we do but if the element type has changed or if there +was no corresponding existing DOM element then and only then do we +append a new DOM element. This ensures the actual DOM tree isn't being +swapped out every time we render, only the elements that change +are. When a new DOM element is appended to the tree React would invoke +~componentDidMount~. + +Next up we'll go through all the auxiliary methods that complete the +implementation. + +Removing a DOM element is straightforward; we just ~removeChild~ on +the parent element. In React, before removing the element, it invoke +~componentWillUnmount~. + #+BEGIN_SRC javascript function removeDOMElement(prevElement) { prevElement.parent.removeChild(prevElement.domElement); } #+END_SRC +In creating a new DOM element we just need to branch if we are +creating a text element since the browser API differs slightly. We +also populate the text element's value as the API requires the first +argument to be specified even though later on when we set props we +will set it again. This is where React would invoke +~componentWillMount~. + #+BEGIN_SRC javascript function createDOMElement(element) { return element.type === 'TEXT' ? @@ -522,6 +561,11 @@ function createDOMElement(element) { } #+END_SRC +To set the props on an element we first clear all the existing props +and then loop through the current props, setting them accordingly. Of +course we filter out the ~children~ prop since we use that elsewhere +and it isn't intended to be set directly. + #+BEGIN_SRC javascript function setDOMProps(element, domElement, prevElement) { if (prevElement) { @@ -539,6 +583,21 @@ function setDOMProps(element, domElement, prevElement) { } #+END_SRC +TODO note that React is more intelligent settings props, it only +updates the ones that need to update + +TODO this algorithm doesn't correctly handle event handler props but +we're ignoring that for simplicity + +For rendering children we use two loops. The first loop removes any +elements that are no longer being used. This would happen when the +number of children is decreased. The second loop starts at the first +child and then iterates through all of the current children calling +~render_internal~ on each child. When ~render_internal~ is called the +corresponding previous element in that position is passed to +~render_internal~ or ~undefined~ if there is no corresponding element, +like when the list of children has grown. + #+BEGIN_SRC javascript function renderChildren(element, domElement, prevElement = { props: { children: [] }}) { const elementLen = element.props.children.length; @@ -555,11 +614,52 @@ function renderChildren(element, domElement, prevElement = { props: { children: } #+END_SRC -TODO don't figure event handlers are handled specially - -** Commit Phase +It's very important to understand the algorithm used here because this +is essentially what happens in React when incorrect keys are used, +like a list index. And this is why keys are so critical to high +performance (and correct) React code. For example, in our algorithm +here, if you removed an item from the front of the list you may cause +every element in the list to be created anew in the DOM if the types +no longer match up. Later on, in the chapter on keys, we will update +this algorithm to incorporate keys. It's actually only a minor +difference in determining which ~child~ gets paired with which +~prevChild~. Otherwise this is effectively the same algorithm React +uses when rendering lists of children. + +There are a few things to note here. First it is important to pay +attention to when React will be removing a DOM element from the tree +and adding a new one as this is when the related lifecycle events are +invoked. And invoking those lifecycle methods, and the whole process +of tearing down and building up a component is expensive. So again you +can see how a bad key would lead to another performance bottleneck +since React will be doing this on all or many of the elements in a +list frequently. ** Fibers + +The actual React implementation used to look very similar to what +we've gone through so far but with React 16 this has changed +dramatically with the introduction of Fibers. Fibers are a name that +React gives to discrete units of work. And the React reconciliation +algorithm was changed to be based on small units of work instead of +one large, potentially long-running call to ~render~. This means that +React is now able to process just part of the render phase, pause to +let the browser take care of other things, and resume again. This is +the underlying change the enables the experimental Concurrent Mode. + +But even with such a large change, the underlying algorithms for +deciding how and when to render components is the same. And when not +running in Concurrent Mode the effect is still the same as React does +the render phase in one block still. So using a simplified +interpretation that doesn't include all the complexities of breaking +up the process in to chunks enables us to see more clearly how the +process as a whole works. At this point bottlenecks are much more +likely to occur from the underlying algorithms and not from the Fiber +specific details. In the chapter on Concurrent Mode we will go in to +this more. + +TODO maybe a graphic summarizing the heuristics? + * Rendering Model :PROPERTIES: :EXPORT_FILE_NAME: manuscript/rendering-model.markua diff --git a/manuscript/fundamentals--building-our-own-react.markua b/manuscript/fundamentals--building-our-own-react.markua index 943a8b1..9eb0475 100644 --- a/manuscript/fundamentals--building-our-own-react.markua +++ b/manuscript/fundamentals--building-our-own-react.markua @@ -232,9 +232,9 @@ else if (element.type !== prevElement.type) Notice that in every case, except deletion, we still call `render` on the element's children. While its possible that the children will be able to reuse their associated DOM elements, `render` will still be run on them. -Now, to get started with our render method we must make some modifications to our previous render methods. First, we need to be able to store and retrieve the previous render tree. Then we need to add code to compare parts of the tree to decide if we need to re-render something or if we can re-use it from the previous render tree. +Now, to get started with our render method we must make some modifications to our previous render methods. First, we need to be able to store and retrieve the previous render tree. Then we need to add code to compare parts of the tree to decide if we need to re-render something or if we can re-use DOM elements from the previous render tree. And last we need to return a tree of elements that can be used in the next render as a comparison and to reference the DOM elements that we create. These new elements will have the same structure as our current elements but we will add two new properties: `domElement` and `parent`. `domElement` is the DOM element associated with our synthetic element and `parent` is a reference to the parent DOM element. -Here we are adding a global object that will store our last render tree, keyed by the `container`. +Here we begin by adding a global object that will store our last render tree, keyed by the `container`. {format: "javascript"} ``` @@ -247,9 +247,7 @@ function render(element, container) { } ``` -TODO note that we are adding parent and domElement properties. - -Now that we have a way to see what we rendered last time we can go ahead and update our render method with the heuristics. +Now that we have stored our last render tree we can go ahead and update our render method with the heuristics for reusing the DOM elements. We name it `render_internal` because it is what controls the rendering but takes an additional argument now: the `prevElement`. `preElement` is a reference to the corresponding `element` from the previous render and contains a reference to its associated DOM element and parent DOM element. If it's the first render or if we are rendering a new node or branch to the tree than `prevElement` will be `undefined`. If, however, `element` is `undefined` and `prevElement` is defined then we know we need to delete a node that previously existed. {format: "javascript"} ``` @@ -262,7 +260,7 @@ function render_internal(element, container, prevElement) { domElement = createDOMElement(element); } else if (element.type === prevElement.type) { domElement = prevElement.domElement; - } else { + } else { // types don't match removeDOMElement(prevElement); domElement = createDOMElement(element); } @@ -285,6 +283,12 @@ function render_internal(element, container, prevElement) { } ``` +The only time we can't set DOM properties on our element and render its children is when we are deleting an existing DOM element. We use this observation to group the calls for `setDOMProps` and `renderChildren`. Choosing when to append a new DOM element to the container is also part of the heuristics. If we can reuse an existing DOM element then we do but if the element type has changed or if there was no corresponding existing DOM element then and only then do we append a new DOM element. This ensures the actual DOM tree isn't being swapped out every time we render, only the elements that change are. When a new DOM element is appended to the tree React would invoke `componentDidMount`. + +Next up we'll go through all the auxiliary methods that complete the implementation. + +Removing a DOM element is straightforward; we just `removeChild` on the parent element. In React, before removing the element, it invoke `componentWillUnmount`. + {format: "javascript"} ``` function removeDOMElement(prevElement) { @@ -292,6 +296,8 @@ function removeDOMElement(prevElement) { } ``` +In creating a new DOM element we just need to branch if we are creating a text element since the browser API differs slightly. We also populate the text element's value as the API requires the first argument to be specified even though later on when we set props we will set it again. This is where React would invoke `componentWillMount`. + {format: "javascript"} ``` function createDOMElement(element) { @@ -301,6 +307,8 @@ function createDOMElement(element) { } ``` +To set the props on an element we first clear all the existing props and then loop through the current props, setting them accordingly. Of course we filter out the `children` prop since we use that elsewhere and it isn't intended to be set directly. + {format: "javascript"} ``` function setDOMProps(element, domElement, prevElement) { @@ -319,6 +327,12 @@ function setDOMProps(element, domElement, prevElement) { } ``` +TODO note that React is more intelligent settings props, it only updates the ones that need to update + +TODO this algorithm doesn't correctly handle event handler props but we're ignoring that for simplicity + +For rendering children we use two loops. The first loop removes any elements that are no longer being used. This would happen when the number of children is decreased. The second loop starts at the first child and then iterates through all of the current children calling `render_internal` on each child. When `render_internal` is called the corresponding previous element in that position is passed to `render_internal` or `undefined` if there is no corresponding element, like when the list of children has grown. + {format: "javascript"} ``` function renderChildren(element, domElement, prevElement = { props: { children: [] }}) { @@ -336,10 +350,16 @@ function renderChildren(element, domElement, prevElement = { props: { children: } ``` -TODO don't figure event handlers are handled specially +It's very important to understand the algorithm used here because this is essentially what happens in React when incorrect keys are used, like a list index. And this is why keys are so critical to high performance (and correct) React code. For example, in our algorithm here, if you removed an item from the front of the list you may cause every element in the list to be created anew in the DOM if the types no longer match up. Later on, in the chapter on keys, we will update this algorithm to incorporate keys. It's actually only a minor difference in determining which `child` gets paired with which `prevChild`. Otherwise this is effectively the same algorithm React uses when rendering lists of children. -## Commit Phase +There are a few things to note here. First it is important to pay attention to when React will be removing a DOM element from the tree and adding a new one as this is when the related lifecycle events are invoked. And invoking those lifecycle methods, and the whole process of tearing down and building up a component is expensive. So again you can see how a bad key would lead to another performance bottleneck since React will be doing this on all or many of the elements in a list frequently. ## Fibers +The actual React implementation used to look very similar to what we've gone through so far but with React 16 this has changed dramatically with the introduction of Fibers. Fibers are a name that React gives to discrete units of work. And the React reconciliation algorithm was changed to be based on small units of work instead of one large, potentially long-running call to `render`. This means that React is now able to process just part of the render phase, pause to let the browser take care of other things, and resume again. This is the underlying change the enables the experimental Concurrent Mode. + +But even with such a large change, the underlying algorithms for deciding how and when to render components is the same. And when not running in Concurrent Mode the effect is still the same as React does the render phase in one block still. So using a simplified interpretation that doesn't include all the complexities of breaking up the process in to chunks enables us to see more clearly how the process as a whole works. At this point bottlenecks are much more likely to occur from the underlying algorithms and not from the Fiber specific details. In the chapter on Concurrent Mode we will go in to this more. + +TODO maybe a graphic summarizing the heuristics? +