You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

277 lines
11 KiB
Org Mode

#+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~
A tale of two trees.
#+BEGIN_SRC javascript
function createElement(node) {
if (typeof node === 'string') {
return {
type: 'TEXT_ELEMENT'
}
const [ tag, props, children ] = node;
const element = document.createDOMElement(tag);
for ([key, val] in props) {
element[key] = val;
}
children.forEach(createElement);
}
#+END_SRC
* 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
* 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