diff --git a/images b/images deleted file mode 120000 index f14623b..0000000 --- a/images +++ /dev/null @@ -1 +0,0 @@ -manuscript/resources/images \ No newline at end of file diff --git a/manuscript/code-splitting.markua b/manuscript/code-splitting.markua deleted file mode 100644 index 3dd102c..0000000 --- a/manuscript/code-splitting.markua +++ /dev/null @@ -1,7 +0,0 @@ -# Code Splitting - -React.lazy, suspense - -use on routes - - diff --git a/manuscript/concurrent-rendering.markua b/manuscript/concurrent-rendering.markua deleted file mode 100644 index 51e2e23..0000000 --- a/manuscript/concurrent-rendering.markua +++ /dev/null @@ -1,3 +0,0 @@ -# Concurrent Rendering - - diff --git a/manuscript/diagnosing-bottlenecks.markua b/manuscript/diagnosing-bottlenecks.markua deleted file mode 100644 index 5d08a66..0000000 --- a/manuscript/diagnosing-bottlenecks.markua +++ /dev/null @@ -1,3 +0,0 @@ -# Diagnosing Bottlenecks - - diff --git a/manuscript/fundamentals--building-our-own-react.markua b/manuscript/fundamentals--building-our-own-react.markua deleted file mode 100644 index 943a8b1..0000000 --- a/manuscript/fundamentals--building-our-own-react.markua +++ /dev/null @@ -1,345 +0,0 @@ -# Fundamentals: 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. - -Understanding: that's what I was missing. The bread I make is now consistently good. The recipes I use are simpler and only give ratios and general recommendations for rests and waits. So why does the bread turn out better? - -Before baking is finished bread is a living organism. The way it grows and develops and flavors depend on what you feed it and how you feed it and massage it, care for it. If you have it grow and ferment at a higher temperature and more yeast it overdevelops producing too much alcohol. If you give it too much time acidity will take over the flavor. The recipes I used initially were missing a critical ingredient: the rising temperature. - -But unlike a lot of ingredients: temperature is hard to control for the home cook. So the recipe can't just tell you exactly what temperature to grow the bread at. My initial recipes just silently made assumptions for the temperature, which rarely turn out to be true. This means that the only way to consistently make good bread is to have an understanding of how bread develops so that you can adjust the other ingredients to complement the temperature. Now the bread can tell me what to do. - -While React isn't technically a living organism that can tell us what to do it is, in its whole, a complex, abstract entity. We could learn basic recipes for how to write high-performance React code but they wouldn't apply in all cases and as React and things under it change our recipes would fall out-of-date. So like the bread, to produce consistently good results we need to understand how React does what it does. - -## React, made of - -Conceptually React is very simple. It starts by walking a tree of components and building up a tree of their output. Then it compares that tree to the tree currently in the browser's DOM to find any differences between them. When it finds differences it updates the browser's DOM to match its internal tree. - -But what does that actually look like? If your app is janky does that explanation point you towards what is wrong? No. It might make you wonder if maybe it is too expensive to re-render the tree or if maybe the diffing React does is slow but you won't really know. When I was initially testing out different bread recipes I had guesses at why it wasn't working but I didn't really figure it out until I had a deeper understanding of how making bread worked. It's time we build up our understanding of how React works so that we can start to answer our questions with solid answers. - -React is centered around the `render` method. The `render` method is what walks our trees, diffs them with the browser's DOM tree, and updates the DOM as needed. But before we can look at the `render` method me have to understand its input. The input comes from `createElement`. While `createElement` itself is unlikely to be a bottleneck it's a good to understand how it works so that we can have a complete picture of the entire process. The more black-boxes we have in our mental model the harder it will be for us to diagnose performance problems. - -## Markup in Javascript: `JSX` - -`createElement`, however, takes as input something that is probably not familiar to us since we usually work in JSX, which is the last element of the chain in this puzzle and the first step in solving it. While not strictly a part of React it is almost universally used with it. And if we understand it then `createElement` will be less of a mystery since we will be able to connect all the dots. - -JSX is not valid HTML or Javascript but its own language compiled by a compiler, like Babel. The output of that compilation is valid Javascript that represents the original markup. - -Before JSX the normal way of injecting HTML into the DOM was via directly utilizing the browser's DOM APIs. This was very cumbersome. The code's structure did not match the structure of the HTML that it output which made it hard to quickly understand what the output of a piece of code would be. So naturally programmers have been endlessly searching for better ways to mix HTML with Javascript. - -And this brings us to JSX. It is nothing new; nothing complicated. Forms of it have been made and used long before React adopted it. Now let's see if we can discover JSX for ourselves. - -To start with we need to create a data structure that both represents a DOM tree and can also be used to insert one into the browser's DOM. And to do that we need to understand what a tree of DOM nodes is constructed of. What parts do you see here? - -{format: "html"} -``` -
-

Hello

- -
-``` - -I see three parts: the name of the tag, the tag's properties, and its children. - -``` -tag name: 'div' -tag prop: 'class' -children: h1..., 'Hello', input... -``` - -Now how could we recreate that in Javascript? - -In Javascript we store lists of things in arrays and key/value properties in objects. Luckily for us Javascript even gives us literal syntax for both so we can easily make a compact DOM tree with our own notation. - -This is what I'm thinking: - -{format: "javascript"} -``` -['div', { 'className': 'header' }, - [['h1', {}, ['Hello']], - ['input', { 'type': 'submit', 'disabled': 'disabled' }, []] - ] -] -``` - -As you can see we have a clear mapping from our notation to the original HTML. Our tree is made up of three element arrays. The first item in the array is the tag, the second is an object containing the tag's properties, and the third is an array of its children; which are all made up of the same three element arrays. - -The truth is though, if you stare at it long enough, although the mapping is clear, how much fun would it be to read and write that on a consistent basis? I can assure you, it is rather not fun. But it has the advantage of being easy to insert into the DOM. All you need to do is write a simple recursive function that ingests our data structure and updates the DOM accordingly. We will get back to this. - -So now we have a way to represent a tree of nodes and we (theoretically) have a way to get those nodes into the DOM. But if we are being honest with ourselves, while functional, it isn't a pretty notation nor easy to work with. - -And this is where our object of study enters the scene. JSX is just a notation that a compiler takes as input and outputs in its place a tree of nodes nearly identical to the notation we came up with! And if you look back to our notation you can see that you can easily embed arbitrary Javascript expressions wherever you want in a node. As you may have realized, that's exactly what the JSX compiler does when it sees curly braces! - -There are three main differences between our data structure and the real one that the JSX compiler outputs: it uses objects instead of arrays, it inserts calls to React.createElement on children, and spreads the children instead of containing them in an array. Here is what "real" JSX compiler output looks like: - -{format: "javascript"} -``` -React.createElement( - 'div', - { className: 'header' }, - React.createElement('h1', {}, 'Hello'), - React.createElement( - 'input', - { type: 'submit', 'disabled': 'disabled' }) -); -``` - -As you can see it is very similar to our data-structure and for the purposes of this book we will use our own simplified data-structure as it's a bit easier to work with. A JSX compiler also does some validation and escapes input to prevent cross-site scripting attacks. In practice though they would behave the same in the ways that matter to us now. - -So now that we've worked through JSX we're ready to tackle `createElement`, the next item on our way to building our own React. - -## Getting Ready to Render with `createElement` - -React's `render` expects to consume a tree of element objects in a specific, uniform format. `createElement` is the method by which we achieve that objective. `createElement` will take as input our JSX-like notation and output a tree of objects compatible with `render`. - -React expects nodes defined as Javascript objects that look like this: - -{format: "javascript"} -``` -{ - type: NODE_TYPE, - props: { - propA: VALUE, - propB: VALUE, - ... - children: STRING | ARRAY - } -} -``` - -That is: an object with two properties: `type` and `props`. The `props` property contains all the properties of the node. The node's `children` are also considered part of its properties. The full version of React's `createElement` includes more properties but they are unlikely to be relevant to your application's performance or our version of React here. - -{format: "javascript"} -``` -function createElement(node) { - // an array: not text, number, or other primitive - if (typeof node === 'object') { - const [ tag, props, children ] = node; - return { - type: tag, - props: { - ...props, - children: children.map(createElement) - } - }; - } - - // primitives like text or number - return { - type: 'TEXT', - props: { - nodeValue: node, - children: [] - } - }; -} -``` - -Our `createElement` has two main parts: complex elements and primitive elements. The first part tests whether `node` is a complex node (specified by an array) and then generates an `element` object based 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. 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! - -## 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 should be familiar to you: - -{format: "javascript"} -``` -function render(element, container) -``` - -Doing the initial render on a tree of elements is quite simple. In psuedocode it looks like this: - -{format: "javascript"} -``` -function render(element, container) { - const domElement = createDOMElement(element); - setProps(element, domElement); - renderChildren(element, domElement); - container.appendChild(domElement); -``` - -Our DOM element is created first. Then we set the properties, render children elements, and finally append the whole tree to the container. Now we will work on expanding the psuedocode until we build our own fully functional `render` method using the same general algorithm React uses. Next we will focus on the initial render and ignore reconciliation. - -{format: "javascript"} -``` -function render(element, container) { - const { type, props } = element; - - // create the DOM element - const domElement = type === 'TEXT' ? - document.createTextNode(props.nodeValue) : - 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); -} -``` - -We begin by creating 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 our JSX-like notation 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 one more part: reconciliation. - -## 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(n3). This leads to the largest performance related aspect in all of React. - -Since an O(n3) 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. - -According to the React documentation their diffing algorithm is O(n) and based on two major components: - -* Elements of differing types will yield different trees -* You can hint at tree changes with the `key` prop. - -In this section we will focus on the first part: differing types. Later on we will discuss and implement the `key` prop. - -TODO some kind of call-out for big deal - -TODO - -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 React's actual implementation later when we talk about Fibers. - -Before we get into the code changes that implement the heuristics it is important to remember that React *only* looks at an element's type, existence, and key. It does not do any other diffing. It does not diff props. It does not diff sub-trees of modified parents. If you could only take away one thing from this book it would that. - -Here is a more in depth look at the algorithm we will be implementing: - -{format: "javascript"} -``` -if (!element && prevElement) - // delete dom element -else if (element && !prevElement) - // add new dom element, render children -else if (element.type === prevElement.type) - // update dom element, render children -else if (element.type !== prevElement.type) - // replace dom element, render children -``` - -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. - -Here we are adding a global object that will store our last render tree, keyed by the `container`. - -{format: "javascript"} -``` -const renderTrees = {}; -function render(element, container) { - const tree = - render_internal(element, container, renderTrees[container]); - // render complete, store the updated tree - renderTrees[container] = tree; -} -``` - -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. - -{format: "javascript"} -``` -function render_internal(element, container, prevElement) { - let domElement, children; - if (!element && prevElement) { - removeDOMElement(prevElement); - return; - } else if (element && !prevElement) { - domElement = createDOMElement(element); - } else if (element.type === prevElement.type) { - domElement = prevElement.domElement; - } else { - removeDOMElement(prevElement); - domElement = createDOMElement(element); - } - setDOMProps(element, domElement, prevElement); - children = renderChildren(element, domElement, prevElement); - - if (!prevElement || domElement !== prevElement.domElement) { - container.appendChild(domElement); - } - - return { - domElement: domElement, - parent: container, - type: element.type, - props: { - ...element.props, - children: children - } - }; -} -``` - -{format: "javascript"} -``` -function removeDOMElement(prevElement) { - prevElement.parent.removeChild(prevElement.domElement); -} -``` - -{format: "javascript"} -``` -function createDOMElement(element) { - return element.type === 'TEXT' ? - document.createTextNode(element.props.nodeValue) : - document.createElement(element.type); -} -``` - -{format: "javascript"} -``` -function setDOMProps(element, domElement, prevElement) { - if (prevElement) { - Object.keys(prevElement.props) - .filter((key) => key !== 'children') - .forEach((key) => { - domElement[key] = ''; - }); - } - Object.keys(element.props) - .filter((key) => key !== 'children') - .forEach((key) => { - domElement[key] = element.props[key]; - }); -} -``` - -{format: "javascript"} -``` -function renderChildren(element, domElement, prevElement = { props: { children: [] }}) { - const elementLen = element.props.children.length; - const prevElementLen = prevElement.props.children.length; - // remove now unused elements - for (let i = elementLen; i < prevElementLen - elementLen; i++) { - removeDOMElement(element.props.children[i]); - } - // render existing and new elements - return element.props.children.map((child, i) => { - const prevChild = i < prevElementLen ? prevElement.props.children[i] : undefined; - return render_internal(child, domElement, prevChild); - }); -} -``` - -TODO don't figure event handlers are handled specially - -## Commit Phase - -## Fibers - - diff --git a/manuscript/images b/manuscript/images deleted file mode 120000 index 3f44f8c..0000000 --- a/manuscript/images +++ /dev/null @@ -1 +0,0 @@ -resources/images \ No newline at end of file diff --git a/manuscript/improving-dom-merge-performance.markua b/manuscript/improving-dom-merge-performance.markua deleted file mode 100644 index 9f9b139..0000000 --- a/manuscript/improving-dom-merge-performance.markua +++ /dev/null @@ -1,3 +0,0 @@ -# Improving DOM Merge Performance - - diff --git a/manuscript/introduction.markua b/manuscript/introduction.markua deleted file mode 100644 index 3530979..0000000 --- a/manuscript/introduction.markua +++ /dev/null @@ -1,23 +0,0 @@ -# Introduction - -It was the late 90's and I was just a kid visiting my Aunt and Uncle and their family in Denver. The days were packed with endless playing and goofing around. I didn't get to see my cousins much and we were having a good time. But it was the late 90's and the Internet was booming. And my cousin was in on it. - -A "startup", that's what he called it. I didn't understand any of what he was saying about it. Grown-up stuff. Then he showed us the webpage for the startup and I thought that was impressive. - -"How did you make that"? I asked him. I think he was a little confused at first about what I was even talking about but he quickly brought me over to the computer and showed me a screen full of text. - -"You just type HTML, that's how you make the webpage." I thought this was the coolest. - -"What do you type that into? What program is it? Can I do that?" He told me it was easy: just use Notepad. I wasn't going to let him go without some hook I could grab into this alien world. He told me it's really easy to learn: do an AOL search for "HTML tutorial". - -So began my journey with web development. I AOL searched my way through as many blinking text tutorials as I could find. It wasn't long until I was building AJAX. We had IE 5.5 and 6 and Mozilla Pheonix. And GMail came out. That changed things, now web apps were "legitimate." - -A lot of the technologies and libraries came and went over the years but one thing remained constant in large web apps: poor performance. From the very early days I was timing things with my stop watch. Sometimes things were slow and I had to understand why and how to fix them. Over the years I learned all about the browser's DOM and its APIs and how they work. I learned how jQuery worked and backbone.js and all the rest. I made apps that didn't lag or have jank. - -I was able to do this because I understood the performance implications of the tools and libraries I was using and I learned how to measure performance. I had discovered the recipe for high-performance code. - -And that is what this book is: a recipe for producing high-performance React applications. First, we learn how React works. Then we learn how to measure performance. And last we learn how to address the bottlenecks we find. Parts of any technical book will go stale as 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. - - diff --git a/manuscript/js-performance-tools.markua b/manuscript/js-performance-tools.markua deleted file mode 100644 index 5657336..0000000 --- a/manuscript/js-performance-tools.markua +++ /dev/null @@ -1,3 +0,0 @@ -# JS Performance Tools - - diff --git a/manuscript/js-service-workers.markua b/manuscript/js-service-workers.markua deleted file mode 100644 index bdd2262..0000000 --- a/manuscript/js-service-workers.markua +++ /dev/null @@ -1,3 +0,0 @@ -# JS Service Workers - - diff --git a/manuscript/keys.markua b/manuscript/keys.markua deleted file mode 100644 index a99dde0..0000000 --- a/manuscript/keys.markua +++ /dev/null @@ -1,3 +0,0 @@ -# Keys - - diff --git a/manuscript/performance-tools.markua b/manuscript/performance-tools.markua deleted file mode 100644 index 21bf6aa..0000000 --- a/manuscript/performance-tools.markua +++ /dev/null @@ -1,5 +0,0 @@ -# Performance Tools - -trace from scheduler/tracing/profiler component - - diff --git a/manuscript/preface.markua b/manuscript/preface.markua deleted file mode 100644 index 56fb40e..0000000 --- a/manuscript/preface.markua +++ /dev/null @@ -1,3 +0,0 @@ -# Preface - - diff --git a/manuscript/reconciliation.markua b/manuscript/reconciliation.markua deleted file mode 100644 index e97c4c0..0000000 --- a/manuscript/reconciliation.markua +++ /dev/null @@ -1,9 +0,0 @@ -# Reconciliation - -* diffing algorithm based on heuristics. generic algorithm is O(n3) -* "Fiber" algorithm notes - * lists reordering without key means full list output/update - * type changes cause full re-render - * keys should be stable, predictable, unique - - diff --git a/manuscript/reducing-number-of-components.markua b/manuscript/reducing-number-of-components.markua deleted file mode 100644 index 347e9e2..0000000 --- a/manuscript/reducing-number-of-components.markua +++ /dev/null @@ -1,5 +0,0 @@ -# Reducing Number of Components - -higher-order components - - diff --git a/manuscript/reducing-renders.markua b/manuscript/reducing-renders.markua deleted file mode 100644 index e78a74f..0000000 --- a/manuscript/reducing-renders.markua +++ /dev/null @@ -1,3 +0,0 @@ -# Reducing Renders - - diff --git a/manuscript/rendering-model.markua b/manuscript/rendering-model.markua deleted file mode 100644 index d297f04..0000000 --- a/manuscript/rendering-model.markua +++ /dev/null @@ -1,7 +0,0 @@ -# Rendering Model - -React calls shouldComponentUpdate to know if it should re-render the component. by default it returns true. - -generally use PureComponent/React.memo - - diff --git a/manuscript/server-side-rendering.markua b/manuscript/server-side-rendering.markua deleted file mode 100644 index 4162c35..0000000 --- a/manuscript/server-side-rendering.markua +++ /dev/null @@ -1,3 +0,0 @@ -# Server Side Rendering - - diff --git a/manuscript/ux.markua b/manuscript/ux.markua deleted file mode 100644 index 76bc951..0000000 --- a/manuscript/ux.markua +++ /dev/null @@ -1,3 +0,0 @@ -# UX - - diff --git a/manuscript/windowing.markua b/manuscript/windowing.markua deleted file mode 100644 index a9b6b41..0000000 --- a/manuscript/windowing.markua +++ /dev/null @@ -1,3 +0,0 @@ -# Windowing - -