Codementor Events

Building a light weight mobile friendly React DOM

Published Jul 03, 2018Last updated Dec 30, 2018

Good design is obvious. Great design is transparent. But how do we build such designs when an megabyte of your bundle has to load in the user's browser over unreliable network connections ¯_(ツ)_/¯

Enough has been said about reducing the application size. There are tried and tested ways reducing bundle size via minification, dead code elimination, code splitting, et cetera. As we started bundling large applications these techniques have proven necessary over time. But luckily with React that's not the end of it!
React 16.1 made a first release of react-reconciler: a library that lets you use React's core to write a renderer that renders into any environment and not just the DOM. It was an already known fact that React is environment agnostic: React Native was the success story testifying it. But with react-reconciler anyone can renderers for any host environment they like. A popular attempt at explaining how this is done is Dustan Kasten's Tiny React Renderer (I am sure there are more). It is now possible to write renderers for firmata-based hardware, pdfs, QML, Regl, Framer Animations and so. So why not attempt writing another renderer targeted for the DOM itself, but this time starting with bare minimum, focusing on low powered devices, making sure it's lightweight.
And that's the story of React DOM Lite. Let's take a look at how it works.

Here's the renderer file
You can test it quickly by creating a react app using create-react-app and using the render function from our new renderer instead of the import { render } from 'react-dom'

The API

First off, let's be clear with one thing: everything here is simplified. I have tried my best to be factually correct, but the pitfalls of simplification must not be overlooked. You are encouraged to dig deeper or reach out to us. This tutorial is not the end, but merely a beginning in this learning process.

The render function: .render()

Let's start from where we already are familiar: the render function.react-dom's render method works by creating a container for react managed DOM tree using renderer.createContainer and creates the DOM nodes inside it using renderer.updateContainer <The mount must be unbatched>
While I think the code speaks for itself, in the upcoming sections I have tried to rationalise them retrospectively go through the source. I hope these mental models help you too 😃

Instantiating the renderer

This is done with the factory function exposed as default on react-reconciler This function takes an object whose properties can broadly by classified into 3 categories:

  1. Functions that help mutate the Host Data Structures (DOM tree)
  2. Calculating mutations to be pushed to Host environment (DOM)
  3. Hydrating the already rendered DOM.

Functions manipulating the Host Data Structures

Host environments often provide an imperative API for us to render shapes, text and other such elements. The DOM is no different. What is important however to note is that DOM provides us with retained mode graphics. Often contrasted via immediate mode graphics that HTML5 canvas developers are familiar with, updating UI built on DOM doesn't require us to build frame-by-frame logic. We are given a tree like data structure and we add, delete and modify the nodes in the tree. Given how manipulating the nodes are dependent on Host environment itself, we need to provide React with ways to do exactly that: updating properties/attributes (using commitUpdate), appending child (appendChild and appendChildToContainer), inserting before (insertBefore and insertInContainerBefore, removing a child (removeChild and removeChildFromContainer). An alternative way to looking at these operations: these are routines that are run during the commit phase once all the mutations are calculated.

While the above describe updating an already existing DOM, it is methods like appendInitialChild, createInstance (creating the most basic node in the tree, in our case a DOM element), createTextInstance, finalizeInitialChildren (applying initial props onto the DOM tree), along with some read only operations like getRootHostContext, getChildHostContext, getPublicInstance that help build the tree in the first place.

Calculating mutations to be pushed to the DOM

The mutations that we pushed to DOM have to be calculated in the first place. Again, this calculation is host specific - any update to the UI is finally nothing but the change in the underlying data structure. And this depends on the Host again. 
prepareUpdate solely handles this task. It's only objective is to return a data structure that React's core algorithm can schedule for future mutations. Refer diffProps function to get a vague understanding of what a diffing algorithm needs to look like with react-reconciler.

For the sake brevity, I'm not covering hydration in this post. Definitely a topic for future posts 😃

Oh by the way a disclaimer: the opening quote "Good design is obvious. Great design is transparent." is attributed to Joe Sparano, graphic designer for Oxide Design Co. In no ways did I intend to sound as if it were mine 😄
Feel free to reach out to us. I quietly live under the twitter handle @ManasJayanth. We would love it if you drop by React DOM Lite and share your thoughts.

Discover and read more posts from Manas Jayanth
get started