React is a JavaScript library for building user interfaces. Let's break down how it operates behind the scenes in a simple way.
When we open a web page, browser receives an HTML document from server, then constructs a logical, tree-like structure to show the page known as the Document Object Model (DOM)
User needs to interact with the structure, style, and content of web pages, so DOM update is needed, using the DOM API. While the DOM API itself is not inherently slow, frequent updates to the DOM are costly because the browser has to recalculate CSS, do layout, and repaint the screen, especially in complex UIs
React's solution is to use a virtual DOM. Instead of updating the DOM directly, React creates a virtual DOM from new state of UI and compares with previous virtual DOM, then updates only changed objects in real DOM, which makes the process faster.
When you write JSX, you’re using either functional or class components in React. React internally invokes:
FunctionComponent(props)
for functional components.classComponentInstance.render()
for class components.Both are ultimately transformed into React.createElement()
calls, which return React elements (virtual DOM).
For example:
// JSX:
return <Component anyProp1={26} anyProp2="example">Some text</Component>
// Translated to:
return React.createElement(Component, {anyProp1: 26, anyProp2: "example"}, "Some text")
// React element object:
{type: Component, props: {anyProp1: 26, anyProp2: "example"}, children: ["Some text"]}
So basically;
Compilers like Babel, Vite, and Esbuild convert JSX into React.createElement
calls. This conversion allows React to work with plain JavaScript objects that represent the virtual DOM.
Lets try to illustrate the concept by writing very simple & dummy compiler like code. Please keep in mind; this no near to actual implementation, the aim is making you familiar with the concept.
const fs = require("fs"); // Read jsx file
const regex = /["']/g; // Regex to remove quotes
// Convert JSX to React.createElement calls
function compileJSX(jsx) {
let compiled = jsx;
// Handle self-closing tags
compiled = compiled.replace(/<(\w+)([^>]*)\/>/g, (match, tag, attributes) => {
return `React.createElement('${tag}', {${parseAttributes(attributes)}}), `;
});
// Handle opening tags
compiled = compiled.replace(/<(\w+)([^>]*)>/g, (match, tag, attributes) => {
return `React.createElement('${tag}', {${parseAttributes(attributes)}}, `;
});
// Handle closing tags
compiled = compiled.replace(/<\/\w+>/g, "), ");
// Handle text content
compiled = compiled.replace(/>([^<]+)</g, (match, text) => {
return `, '${text.trim()}', `;
});
// Remove new lines and extra spaces
compiled = compiled.split("\n");
compiled.splice(0, 2);
compiled.splice(-2);
const paddingLength = compiled[0].match(/^\s+/)[0].length;
compiled = compiled
.filter((n) => n !== "")
.map((n) => n.slice(paddingLength))
.join("\n");
return compiled;
}
function parseAttributes(attributes) {
if (!attributes.trim()) return "";
// Convert attributes to object
return attributes
.trim()
.split(/\s+/)
.map((attr) => {
const [key, value] = attr.split("=");
return `${key}: ${value.replace(regex, "")}`;
})
.join(", ");
}
// Read JSX from a file
const jsxFileContent = fs.readFileSync("ExampleComponent.jsx", "utf8");
// Compile JSX to React.createElement calls
const compiledCode = compileJSX(jsxFileContent);
// Print the compiled code
console.log(compiledCode);
The output for a component like:
const ExampleComponent = ({ title, children, imageUrl }) => {
return (
<div>
<h1>{title}</h1>
<img src="https://example.com/example.jpg" alt="Example_visual_content" />
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<button>Click me</button>
</div>
);
};
Would be:
React.createElement('div', {},
React.createElement('h1', {}, {title})
React.createElement('img', {src: 'https://example.com/example.jpg', alt: Example_visual_content})
React.createElement('ul', {},
React.createElement('li', {}, Item 1)
React.createElement('li', {}, Item 2)
React.createElement('li', {}, Item 3)
)
React.createElement('button', {}, Click me)
)
)
Once the output is executed, React will generate React Elements
.
So congrats, we wrote our first dummy compiler-like code.
So far in summary;
React.createElement()
can be calledReact.createElement()
called and created a JS object which represents virtual DOM.Now, its time for actual DOM comes into play.
As next step after generating Virtual DOM, React will diff the virtual DOM, and collects a list of all the changes that need to be applied to the real DOM. The diffing and calculation process is known as "reconciliation". We can divide it into 2 phases;
For re-rendering due to performance measures, React needs to transform 1 tree into another with minimum operations.
Tree comparison algorithms generally have high complexity, often O(n^3)
;
React developers designed an algorith using Heuristic algorithm with complexity O(n)
best case
To achieve this, there are several assumptions to optimize the performance for common cases;
key
prop can say which element stay stable in between renders** Above assumptions are true for most of the cases
Understanding these assumptions can help you write more performant React code.
For example lets say we want to render ExampleComponent
with different modes (edit & create);
const ChildrenComponent = () => {
useEffect(() => {
console.log("component MOUNT");
return () => {
console.log("component UNMOUNT");
};
}, []);
return (
<img src="https://example.com/example.jpg" alt="Example_visual_content" />
);
};
const ExampleComponent = (mode) => {
if (mode === "edit") {
return (
<div className="container">
<div className="image">
<ChildrenComponent />
<DeleteUpdateControls />
</div>
</div>
);
}
return (
<div className="container">
<ChildrenComponent />
</div>
);
};
If mode
prop changes for rendering ExampleComponent
, the algorithm revise the component from top to bottom. First one is <div className="container">
and it remains but <div className="image">
and <ChildrenComponent />
makes change of types, so it means;
img
component is removedcomponent UNMOUNT
loggedcomponent MOUNT
logged.Lets now remove the <div className="image">
const ExampleComponent = () => {
if (mode === "edit") {
return (
<div className="container">
<ChildrenComponent />
<DeleteUpdateControls />
</div>
);
}
return (
<div className="container">
<ChildrenComponent />
</div>
);
};
Now it will optimize a bit and our image component will not unmounted and mounted everytime, mode changes.
But what if the component was like;
const ExampleComponent = () => {
if (mode === "edit") {
return (
<div className="container">
<DeleteUpdateControls />
<ChildrenComponent />
</div>
);
}
return (
<div className="container">
<ChildrenComponent />
</div>
);
};
React will again makes comparison top to bottom and see a change in types and again;
img
component is removedcomponent UNMOUNT
loggedcomponent MOUNT
logged.If we change a code little;
const ExampleComponent = () => {
return (
<div className="container">
{mode === "edit" && <DeleteUpdateControls />}
<ChildrenComponent />
</div>
);
};
Now in the tree, the place is reserved for boolean, although its not visible, and the position of image component will always same (unnecessary mount and unmount wont happen).
So as a result knowing the assumptions (in heuristic approach) will make you solve strange behaviours at first glance.
Allows updates to be prioritized based on urgency, but it is not the case with synchronous rendering, which is used by default in earlier versions of React. After the virtual DOM creation and comparison with actual DOM and extracting which nodes should be updated before re-rendering, there is one more step called prioritization
. This is another topic, I wont go in details but in short;
After extracting the nodes need to be updated, React makes prioritization according to the urgency of their getting to the user because we want the application not to lag. So React makes ordering of tasks (update of nodes) from high to low priority and updates the tree in that order, so change will be smooth.
So far in summary;
React.createElement()
can be calledReact.createElement()
called and created a JS object which represents virtual DOM.Once the changes to the virtual DOM are identified, React enters the commit phase, where it updates the actual DOM. On initial rendering uses multiple DOM operations based on what elements are being rendered, which might include appendChild()
, setAttribute()
, removeChild()
, etc.. For re-renders, the minimal necessary operations are applied. React library does not communicate with the real DOM directly, instead it uses Renderers like React DOM (for web platforms) and React Native (for mobile platforms).
React schedules rendering work asynchronously in the "render phase" and prioritizes tasks (introduced in React 18), but once it reaches the commit phase, the updates are applied synchronously, meaning it occurs in one go without interruption to ensure that the DOM never displays partial results for consistent UI. After manipulation of actual DOM the browser recalculates styles, performs layout and repaints the screen to reflect the updated UI. This process is handled internally by the browser and is independent of React.
Some phases (mentioned above) managed by React, some phases involves other packages (like ReactDOM). This is why we import both React and ReactDOM.
In this post, we covered the basic mechanics of React's virtual DOM, the reconciliation process, and how React efficiently updates the real DOM through the commit phase. By understanding React's underlying architecture, you can write better-optimized code and troubleshoot performance issues more effectively.
Thanks for reading it, hope you like it. If you want to discuss about any information above, drop an email to sinantalhakosar[at]gmail.com. Cheers 🥳