React In Modern Browsers
Using React for the first time can be confusing and exciting. It was for me, but as I ventured deeper into hooks, I started to wonder where state variables are stored and what the life-cycle of a state variable represents. Here, I will guide us through a path of discovery.
Why Google Chrome?
We will limit the scope to Google Chrome, as it’s the most popular browser in 2020 and occupies 66% of the market. GC’s JavaScript engine is called V8, the same system used in NodeJS. FireFox uses the JS engine SpiderMonkey — which is similar to V8 with more decompiler phases.
Some Basic Terminology
Central Processing Unit — can be considered the brain of a computer.
Graphics Processing Unit is hardware that renders images.
Document Object Model is a tree data structure representation of the web-page and lives in memory allocated by the operating system.
JS Object is a data type with a key-value pair. Key can be a number or string, and value can be any data type.
React is a front-end JS library for building web applications.
A Thread is a single unit that lives inside CPU memory and belongs to a process. Processes get divided into threads of execution that can be general or specific, and multiple threads run in parallel.
A Process is a collection of tasks consisting of a single main thread and other supporting threads, all working together to complete a task.
Where Do We Start?
The browser gets the JS code from either a network source or a local path. JS is the select language that GC’s V8 Engine parses into executable code called Abstract Syntax Tree — which is all JS code bundled into a single module. The AST compiles to syntax called bytecode, where it executes in the Interpreter on the main thread. GC uses two processes — browser and renderer, but we’ll talk about the browser process first.
How V8 Optimizes Speed
All JS engines handle objects in the same way. DOM nodes resemble JS objects, and here is an illustration of how JS objects get parsed.
V8 uses an optimizing compiler to create a ‘shape’ (also called hidden class) for each variable declared inside a function or object. Each additional property adds a node to a chain of shapes to construct an overall object. When multiple instances share similar properties, it’s more efficient to instantiate from the shared shape-chain. Instantiation demands less memory, thus executes faster.
These shapes are the building blocks that live inside CPU memory. When a state changes, the compiler finds the shape-chain associated with that state, creating a new object based on the original shape-chain, then updates the DOM inside the CPU. The main goal is to update the DOM data, which the renderer uses to produce the image we see.
What Renders The Page?
I mentioned earlier that there is a second process to GC called the renderer, and it has its separate threads. The renderer’s two primary process-threads are responsible for rasterizing and compositing.
What Are These Two Threads?
First off, the renderer’s main thread fetches the DOM from memory and separates the tree into layers. Each layer divides into tiles, a data structure that holds the graphical representation, position, and memory location (also called draw quads). It is the raster thread’s job to paint each tile, then store it in the GPU at the specified memory location. The compositor’s job is to assemble these tiles on the page and produce the user’s view. This cycle occurs 60 times per second — according to GC’s frame rate.
How About Scrolling?
Additionally, the compositor’s job is to handle what the user sees at any part of the page. When the frame changes, the compositor thread sends a fetch request to the browser process, which accesses the GPU for the frame tiles.
However, user input events are different. Events such as on-click and mouseover register under the browser process. The event-type and (x, y) position gets sent to the renderer process. The renderer uses the Blink engine to handle event-based JS, then updates the DOM and renders a result.
Where’s The React Part?
I’m glad you asked. React variables and states are treated just like any ordinary JS, where state variables come with lifecycle methods. React creates two separate JSX format — Virtual DOM structures — as accessing GC’s DOM in memory is slow and costly. VDOM nodes are simply JS objects that reflect DOM nodes containing props and state attributes. At any time, one of the two VDOMs is always a representation of the current DOM state — current, the other is used to compare and make changes — work in progress. Both of these VDOMs exist in the CPU memory throughout the tab’s lifetime.
In React, when a state variable updates, the render method is initiated and a series of React Fiber methods get executed. There are 3 phases in React rendering:
In the V8 engine, the react commit-method magic — reconciliation happens to produce two VDOM structures in memory. At the end of each commit phase, the render method invokes, and changes to VDOM nodes get flushed to the DOM. During the browser process, these JSX elements exist as instances of shapes and update the DOM in batches.
So Where Does State Live?
The title is the main topic I wanted to answer but ended up dancing around it for a while. In summary, the browser application occupies memory — allocated by the operating system and is where all objects live. JS global variables, classes, functions, and the DOM document all exist as children of the window object inside this same container. Even the VDOMs exist as children of the window object. Inside each VDOM lives React state — which is no exception — and thus exists in memory as a variably-distant child of the window object.
Something Is Uncertain
State variables are divided inside the interpreter and compiler threads as instances of shapes. It is uncertain what each V8 thread’s state is at any given time, a sort of Schroedinger’s Cat situation. We know each JS variable utilized in computing must exist inside the browser process’ memory container, we are just not sure where exactly… but it’s there.
Thanks for reading!
Coding Helps GC’s Renderer
- Avoid using setTimeout and setInterval — they interfere with GC’s FPS cycle. For async callbacks, GC recommends requestAnimationFrame.
- Initialize JS variables with values similar to their expected values — avoid using null/undefined. Zero and ‘’ are fine, I will not go into the why, but you can read about heap numbers here.
- Use the Performance-Tab and Lighthouse GC Dev-Tools extensions to measure performance, accessibility, and best practices.
- Avoid deleting properties and declare them in the constructor on instantiation.
Learn More Here:
[1] https://v8.dev/blog/react-cliff
[3] https://v8.dev/blog/launching-ignition-and-turbofan
[5] https://richardartoul.github.io/jekyll/update/2015/04/26/hidden-classes.html
[6] https://developers.google.com/web/updates/2018/09/inside-browser-part1
[8] https://html.spec.whatwg.org/multipage/parsing.html#overview-of-the-parsing-model
[9] https://mathiasbynens.be/notes/shapes-ics
[10] https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution