From f3a483fe027dcb1a0ffaf9031d78c13ea5dccfa1 Mon Sep 17 00:00:00 2001 From: Thomas Hintz Date: Sat, 25 Jul 2020 11:28:53 -0700 Subject: [PATCH] Adding to Render. --- high-performance-react.org | 212 ++++++++++++++++++++++++++++++++++++- 1 file changed, 209 insertions(+), 3 deletions(-) diff --git a/high-performance-react.org b/high-performance-react.org index dec29cf..c23d622 100644 --- a/high-performance-react.org +++ b/high-performance-react.org @@ -53,6 +53,9 @@ technology changes and that is no less true for this book. But what I hope you learn is not just the technical details but more importantly the method for writing high-performance code. The API might change but the method will remain the same. + +TODO note that the book references React-DOM but the algorithms should +generally apply to all React implementations. * Mini 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 @@ -288,13 +291,216 @@ elements. The first part tests whether ~node~ is a complex node on the input node. It recursively calls ~createElement~ to generate an array of children elements. If the node is not complex then we generate an element of type 'TEXT' which we use for all primitives -like strings and numbers. +like strings and numbers. We call the output of ~createElement~ a tree +of ~elements~ (surprise). That's it. Now we have everything we need to actually begin the process of rendering our tree to the DOM! -*** Reconciliation -A tale of two trees. +** Render + +There are now only two major puzzles remaining in our quest for our +own React. The next piece is: ~render~: how do we go from our tree of +nodes to actually displaying something on screen? + +The signature for our ~render~ method is very simple and will be +familiar to you: + +#+BEGIN_SRC javascript +function render(element, container) +#+END_SRC + +Doing the initial render on a tree of elements is quite simple. In +psuedocode it looks like this: + +#+BEGIN_SRC javascript +function render(element, container) { + const domElement = createDOMElement(element); + setProps(element, domElement); + renderChildren(element, domElement); + container.appendChild(domElement); +#+END_SRC + +Because the browser APIs for text elements are different than for generic +DOM elements and because text elements can't have children we will +split up the process in to two methods: ~renderTextElement~ and +~renderDOMElement~. + +#+BEGIN_SRC javascript +function render(element, container) { + if (element.type === 'TEXT') { + renderTextElement(element, container); + } else { + renderDOMElement(element, container); + } +} +#+END_SRC + +First, we'll look at ~renterTextElement~, which is the simpler of the +two. + +#+BEGIN_SRC javascript +function renderTextElement(element, container) { + return container.appendChild( + document.createTextNode(element.props.nodeValue)); +} +#+END_SRC + +~renderTextElement~ just creates a DOM ~TextNode~ and appends it to +the container. + +Next, we look at renderDOMElement which must also set properties on +the newly created DOM element and render any children. + +#+BEGIN_SRC javascript +function renderDOMElement(element, container) { + const { type, props } = element; + + // create the DOM element + const domElement = document.createElement(type); + + // set its properties + Object.keys(props) + .filter((key) => key !== 'children') + .forEach((key) => { + domElement[key] = props[key]; + }); + + // render its children + props.children.forEach((child) => render(child, domElement)); + + // add our tree to the DOM! + container.appendChild(domElement); +} +#+END_SRC + +To start with we create the DOM element. Then we need to set its +properties. To do this we first need to filter out the ~children~ +property and then we simply loop over they keys setting each property +directly. Then we render each of the children by looping over the +children recursively calling ~render~ on each with the ~container~ set +to the current DOM element (which is each child's parent). + +Now we can go all the way from JSX to a rendered tree in the browser's +DOM! But so far we can only add things to our tree. To be able to +remove and modify the tree we need two more parts: reconciliation and +the commit phase. + +** Reconciliation +A tale of two trees. These are the two trees that people most often +talk about when talking about React's "secret sauce": the VDOM or +virtual DOM and the current render tree. This idea is what originally +set React apart. React's reconciliation is what allows you to program +declaratively. Reconciliation is what makes it so we no longer have to +manually update and modify the DOM whenever our own internal state +changes and in a lot of ways is that makes React, React. + +Conceptually the way this works is that React generates a new element +tree for every render and compares to the newly generated tree to the +tree generated on the previous render. Where it finds differences in +the tree it knows to mutate the DOM state. This is the "tree diffing" +algorithm. + +Unfortunately those researching tree diffing in Computer Science have +not yet produced a generic algorithm with sufficient performance for +use in something like React as the current best still runs in +O(n^3). This leads to the largest performance related aspect in all of +React. + +Since an O(n^3) algorithm isn't going to cut it the creators of React +instead use a set of heuristics to determine what parts of the tree +have changed. Understanding how the React tree diffing algorithm works +in general and the heuristics currently in use can help immensely in +detecting and fixing React performance bottlenecks. And beyond that it +can help one's understanding of some of React's quirks and usage. Even +though this algorithm is internal to React and can be changed anytime +its details have leaked out in some ways and are overall unlikely to +change in major ways without larger changes to React. + +TODO some kind of call-out for big deal + +TODO https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf + +The approach we will take here is to integrate the heuristics that +React uses into our render method. This is similar to how React itself +does it and we will discuss that later when we talk about Fibers. + +To do this we must make some modifications to our 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 +tree keyed by the container. + +#+BEGIN_SRC javascript +const renderTrees = {}; +function render(element, container) { + const tree = + render_internal(element, container, renderTrees[container]); + // render complete, store the updated tree + renderTrees[container] = tree; +} + +function render_internal(element, container, prevElement) { + if (element.type === 'TEXT') { + return renderTextElement(element, container, prevElement); + } else { + return renderDOMElement(element, container, prevElement); + } +} +#+END_SRC + +TODO psuedo-code for heuristics +#+BEGIN_SRC javascript + if (!element && prevElement) + // delete dom element + else if (element && !prevElement) + // add new dom element + else if (element.type === prevElement.type) + // update dom element +#+END_SRC + +Now that we have a way to see what we rendered last time we can go +ahead and update our render methods with the heuristics. + +#+BEGIN_SRC javascript +function renderTextElement(element, container, prevElement) { + const { nodeValue } = element.props; + let domElement; + if (element && prevElement && + element.type === prevElement.type && + prevElement.props.nodeValue && + nodeValue !== prevElement.props.nodeValue) { + // types match but values don't; update + prevElement.domElement.nodeValue = nodeValue; + domElement = prevElement.domElement; + } else { + if (element && prevElement && + element.type !== prevElement.type) { + // element types don't match so remove & append + prevElement.parent.removeChild(prevElement.domElement); + } else if () { + // TODO delete node + } + // new type or new text node + domElement = + container.appendChild(document.createTextNode(nodeValue)); + } + return { + domElement: domElement, + parent: container, + ...element + }; +} +#+END_SRC + +TODO don't figure event handlers are handled specially + +** Commit Phase + +** Fibers * Rendering Model React calls shouldComponentUpdate to know if it should re-render the component. by default it returns true.