Alright, you know react uses virtual DOM to improve rendering performance. In this article we will try to understand how virtual DOM works and what are those design choices which actually end up improving the rendering performance.
But before that, lets understand how rendering works -
Browser keep tracks of the changes happening to the web page and whenever they happen, it repaints the page by rebuilding the DOM. This operation of detecting changes, calculating layout and repainting the elements is a expensive operation as the whole DOM is rebuilt event though only a part of it has changed.
So everytime a change happens, the browser has to -
Compute css rules and styles for every element
Calculate position of each element in the page layout
Repaint the view port by handling changes in layers, visual effects, transparency etc.
Finally combine all the layers and display the page
Let's take an example to understand this better.
Following code snippet contains a simple login form with username, password input fields and a submit button.
I also have added a mutation observer to detect changes in the DOM.
Try to enter a value in the username field and observe the changes in the DOM. You would notice that the mutation observer is triggered for every character you enter in the input fields and for the button click as well.
<!DOCTYPE html>
<html><head><title>React Login</title><scriptsrc="https://unpkg.com/react@18/umd/react.development.js"></script><scriptsrc="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script><scriptsrc="https://unpkg.com/@babel/standalone/babel.min.js"></script><style>
.error {color:red;}</style></head><body><divid="root"></div><script>// Set up the observerconstobserver = new MutationObserver((mutations)=>{mutations.forEach((mutation)=>{console.log('DOM changed:',mutation.type,mutation.target);});});// Start observing with configobserver.observe(document.body,{subtree:true,// Watch all descendantschildList:true,// Watch for node additions/removalsattributes:true,// Watch for attribute changescharacterData:true,// Watch for text changesattributeOldValue:true// Record old attribute value});</script><scripttype="text/babel">functionLoginForm(){const[username,setUsername] = React.useState('');const[password,setPassword] = React.useState('');const[errors,setErrors] = React.useState({});constvalidate = ()=>{constnewErrors = {};if(username.length < 3)newErrors.username = 'Username too short';if(password.length < 6)newErrors.password = 'Password too short';setErrors(newErrors);returnObject.keys(newErrors).length === 0;};consthandleSubmit = (e)=>{e.preventDefault();if(validate()){alert('Login successful!');}};return(<formonSubmit={handleSubmit}><div><label>Username:</label><inputtype="text"value={username}onChange={(e)=>setUsername(e.target.value)}/>{errors.username && <spanclassName="error">{errors.username}</span>}</div><div><label>Password:</label><inputtype="password"value={password}onChange={(e)=>setPassword(e.target.value)}/>{errors.password && <spanclassName="error">{errors.password}</span>}</div><buttontype="submit">Login</button></form>);}ReactDOM.createRoot(document.getElementById('root')).render(<LoginForm/>);</script></body></html>
Now this whole process has few performance bottlenecks -
DOM operations are synchronous and block the main thread until completion. When complex DOM operations block this thread, it causes the dreaded "jank" - stuttering animations and unresponsive interfaces.
The DOM lives outside the JavaScript engine. And cross process or boundary communication is expensive and slow.
DOM changes have cascading effects where a change in one element can affect the position of other elements.
How react solves this problem using virtual DOM ?
Well, react maintains a virtual DOM (tree) in memory. This tree is a lightweight javascript object tree.
Whenever there are changes to the state of the application, react first makes the changes to the virtual DOM. Then it basically checks for a difference between the virtual DOM and the actual DOM( diffing algorithm ).
The diffing algorithm typically works by:
Comparing nodes by type and key
Using tree traversal techniques (often depth-first)
Implementing heuristics like:
Different element types will produce different trees
Elements with stable keys maintain their identity across renders
React identifies the minimal set of changes required to update the DOM.
Batch Rendering
Batch rendering takes advantage of JavaScript's event loop architecture: