|
|
|
#+TITLE: High-Performance React
|
|
|
|
#+AUTHOR: Thomas Hintz
|
|
|
|
|
|
|
|
* Preface
|
|
|
|
* 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.
|
|
|
|
* 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
|
|
|
|
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.
|
|
|
|
|
|
|
|
** Basic React
|
|
|
|
|
|
|
|
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 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
|
|
|
|
understand of how React works so that we can start to answer our
|
|
|
|
questions with solid answers.
|
|
|
|
|
|
|
|
React is made up of a few pieces: ~createElement~, ~render~, and
|
|
|
|
reconciliation. The first building block is ~createElement~. While
|
|
|
|
~createElement~ is itself 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.
|
|
|
|
|
|
|
|
** ~JSX~
|
|
|
|
|
|
|
|
But before we get to ~createElement~ we should talk about JSX. 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.
|
|
|
|
|
|
|
|
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?
|
|
|
|
|
|
|
|
TODO include text element (Hello)
|
|
|
|
TODO change to using objects instead of arrays?
|
|
|
|
probably do after what we've already done
|
|
|
|
|
|
|
|
{
|
|
|
|
type: 'h1',
|
|
|
|
props: { x: y },
|
|
|
|
children: []
|
|
|
|
}
|
|
|
|
|
|
|
|
#+BEGIN_SRC html
|
|
|
|
<div class="header">
|
|
|
|
<h1>Hello</h1>
|
|
|
|
<input type="submit" disabled />
|
|
|
|
</div>
|
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
I see three parts: the name of the tag, the tag's properties, and its
|
|
|
|
children. 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:
|
|
|
|
|
|
|
|
#+BEGIN_SRC javascript
|
|
|
|
['div', { 'class': 'header' },
|
|
|
|
[['h1', {}, ['Hello']],
|
|
|
|
['input', { 'type': 'submit', 'disabled': 'disabled' }, []]
|
|
|
|
]
|
|
|
|
]
|
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
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 expression 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 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:
|
|
|
|
|
|
|
|
#+BEGIN_SRC javascript
|
|
|
|
React.createElement(
|
|
|
|
'div',
|
|
|
|
{ className: 'header' },
|
|
|
|
React.createElement('h1', {}, 'Hello'),
|
|
|
|
React.createElement(
|
|
|
|
'input',
|
|
|
|
{ type: 'submit', 'disabled': 'disabled' })
|
|
|
|
);
|
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
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. In practice 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 item on our way to building our own React.
|
|
|
|
|
|
|
|
TODO JSX also does validation and escapes input to prevent XXS
|
|
|
|
|
|
|
|
** ~createElement~
|
|
|
|
|
|
|
|
React expects nodes defined as Javascript objects that look like this:
|
|
|
|
|
|
|
|
#+BEGIN_SRC javascript
|
|
|
|
{
|
|
|
|
type: NODE_TYPE,
|
|
|
|
props: {
|
|
|
|
propA: VALUE,
|
|
|
|
propB: VALUE,
|
|
|
|
...
|
|
|
|
children: STRING | ARRAY
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
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
|
|
|
|
React's ~createElement~ includes more properties but they are unlikely
|
|
|
|
to be relevant to your application's performance or our version of
|
|
|
|
React here.
|
|
|
|
|
|
|
|
#+BEGIN_SRC javascript
|
|
|
|
// React's createElement
|
|
|
|
const ReactElement = function(type, key, ref, self, source, owner, props)
|
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
So all our ~createElement~ needs to do is transform our data structure
|
|
|
|
into the objects that our React expects.
|
|
|
|
|
|
|
|
#+BEGIN_SRC 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: []
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
#+END_SRC
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
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.
|
|
|
|
* Rendering Model
|
|
|
|
React calls shouldComponentUpdate to know if it should re-render the
|
|
|
|
component. by default it returns true.
|
|
|
|
|
|
|
|
generally use PureComponent/React.memo
|
|
|
|
* Diagnosing Bottlenecks
|
|
|
|
* Reducing Renders
|
|
|
|
* Improving DOM Merge Performance
|
|
|
|
* Reducing Number of Components
|
|
|
|
higher-order components
|
|
|
|
* Windowing
|
|
|
|
* Performance Tools
|
|
|
|
trace from scheduler/tracing/profiler component
|
|
|
|
* JS Performance Tools
|
|
|
|
* Code Splitting
|
|
|
|
React.lazy, suspense
|
|
|
|
|
|
|
|
use on routes
|
|
|
|
* Server Side Rendering
|
|
|
|
* Concurrent Rendering
|
|
|
|
* UX
|
|
|
|
* JS Service Workers
|
|
|
|
* Keys
|
|
|
|
* Reconciliation
|
|
|
|
- diffing algorithm based on heuristics. generic algorithm is O(n^3)
|
|
|
|
- "Fiber" algorithm notes
|
|
|
|
- lists reordering without key means full list output/update
|
|
|
|
- type changes cause full re-render
|
|
|
|
- keys should be stable, predictable, unique
|
|
|
|
|