Understanding How Browsers Work: From HTML to Modern Web Apps

Modern web development can seem like magic, but understanding how browsers actually process and render our code is crucial for building efficient web applications. In this comprehensive guide, we'll break down the journey from HTML to pixels on your screen, and explore how modern frameworks evolved to solve common development challenges.
Part 1: The Browser's Rendering Pipeline in Detail
How Does HTML Load?
When you serve an HTML file on a port and open it in a browser, a complex series of operations begins. Let's break down this process step by step:
HTML Parsing
The browser's HTML parser reads the document character by character
It identifies tags, attributes, and text content
The parser is designed to be forgiving of common HTML mistakes
DOM Construction
The parser converts HTML into the Document Object Model (DOM)
The DOM is implemented in C++ and stored in memory
It creates a tree structure where each HTML element becomes a node
This tree structure allows for efficient traversal and manipulation
Layout Engine Processing
The browser's layout engine processes the DOM
It calculates positions, sizes, and relationships between elements
This process is also known as "reflow"
Rendering
The rendering engine creates bitmap images of the content
These images are typically rendered at 60 FPS for smooth display
The final result is what users see on their screens
How Do HTML and CSS Load Together?
When HTML includes CSS, the process becomes more complex:
Parallel Processing
While HTML is being parsed into the DOM
CSS is simultaneously parsed into the CSSOM (CSS Object Model)
Both structures exist as separate trees in memory
CSSOM Construction
Similar to DOM, but for style rules
Creates a tree structure of all CSS selectors and their properties
Implemented in C++ for performance
Render Tree Creation
The browser combines DOM and CSSOM
Creates a "render tree" containing only visible elements
Applies matching CSS rules to DOM elements
Part 2: JavaScript and DOM Interaction
The Basics of DOM Manipulation
JavaScript provides bindings to interact with the DOM since it's implemented in C++. Let's explore this interaction in detail:
// Basic DOM Manipulation Example
let heading = "hi";
const headingEl = document.querySelector("h1");
headingEl.textContent = heading;
What happens behind the scenes:
JavaScript engine finds the DOM node using C++ bindings
Updates the node's properties - this marks the DOM as changed
The layout and rendering engine detects DOM changes and creates new bitmap images
Triggers a rerender and displays on screen
Adding Dynamic Text with User Input
Let's build on this with a more interactive example:
// Complete example of dynamic text updating
let heading = "";
const headingEl = document.querySelector("h1");
const inputEl = document.querySelector("input");
const handleInput = () => {
heading = inputEl.value;
headingEl.textContent = heading;
};
inputEl.oninput = handleInput;
Understanding Event Flow
Every time a user types in the input:
The input event fires
Our handler function executes
The DOM updates
The browser re-renders affected elements
Part 3: Modern Development Patterns
The Render Pattern
As applications grow, directly manipulating the DOM for each change becomes unmanageable. This led to the development of the render pattern - a single function that handles all DOM updates. This pattern is used by most modern frameworks:
Key benefits of this pattern:
Centralized DOM updates
Predictable data flow
Easier debugging
Better maintenance
Programmatic Element Creation
You might wonder - in frameworks, we often don't write HTML elements in html file. Here's how we can create the same functionality using pure JavaScript:
let heading = null;
let headingEl = null;
let inputEl = null;
const dataToView = () => {
headingEl = document.createElement('h1');
inputEl = document.createElement('input');
inputEl.oninput = handleInput;
heading = heading === null ? "Please write something" : heading;
inputEl.value = heading;
headingEl.textContent = heading;
document.body.replaceChildren(inputEl, headingEl);
};
const handleInput = () => {
heading = inputEl.value;
dataToView();
};
dataToView();
This approach creates HTML elements programmatically, similar to how modern frameworks work under the hood. The replaceChildren
method efficiently updates our view by replacing all existing elements with our new ones.
This pattern of centralized rendering and programmatic DOM manipulation forms the foundation of many modern web frameworks. Understanding these concepts helps us better grasp how frameworks like React or Vue work internally.
Part 4: Scaling for Large Applications
In the above code, we notice a lack of scalability for larger projects. Let’s refactor and explore a more structured and scalable approach.
Dynamic Element Creation
let heading = "Suhas";
const headingInfo = ["h1", heading];
const convert = (node) => {
const elem = document.createElement(node[0]);
elem.textContent = node[1];
return elem;
};
const headingEl = convert(headingInfo);
document.body.replaceChildren(headingEl);
Here, we create an array describing the element (headingInfo
) and use it to generate a DOM element dynamically.
Adding Scalability with Multiple Elements
To scale this idea, let’s handle multiple elements dynamically:
let heading = "Suhas";
let headingEl = null;
const domEl = [
["input", heading, () => { heading = headingEl.value; }],
["h1", heading],
];
const convert = (node) => {
const elem = document.createElement(node[0]);
elem.textContent = node[1];
elem.value = node[1];
elem.oninput = node[2];
return elem;
};
const inputEl = convert(domEl[0]);
headingEl = convert(domEl[1]);
document.body.replaceChildren(inputEl, headingEl);
This works well for a few elements, but as the application grows, it becomes harder to manage.
Introducing a Virtual DOM
To handle large applications, we can introduce a Virtual DOM (VDOM). A Virtual DOM is a lightweight representation of the actual DOM, kept in memory. All operations are performed on the Virtual DOM, and changes are only synchronized with the real DOM when necessary.
Here’s how we can implement a basic Virtual DOM:
let heading = "Suhas";
let headingEl = null;
const createVDOM = () => ([
["input", heading, () => { heading = headingEl.value; }],
["h1", heading],
]);
const convert = (node) => {
const elem = document.createElement(node[0]);
elem.textContent = node[1];
elem.value = node[1];
elem.oninput = node[2];
return elem;
};
const updateDom = () => {
const vDOM = createVDOM();
const elements = vDOM.map(convert);
document.body.replaceChildren(...elements);
};
With this approach, we only need to update the createVDOM
function to include additional elements. This mirrors how modern frameworks like React handle updates.
Addressing Rerendering
Currently, the DOM does not rerender automatically when changes occur. To simulate updates, we can use setInterval
to call updateDom
periodically:
const updateDom = () => {
const vDOM = createVDOM();
const elements = vDOM.map(convert);
document.body.replaceChildren(...elements);
};
setInterval(updateDom, 10);
Now, changes in the Virtual DOM will reflect in the real DOM every 10 milliseconds.
Optimizing with Diffing Algorithm
However, this approach still regenerates the entire DOM on every update, which is inefficient. Frameworks like React use a diffing algorithm and Hooks ( functions which tells react that changed happened ) to detect changes in the Virtual DOM and only update the modified elements in the actual DOM.
This process is called DOM Reconciliation, and it improves performance by minimizing unnecessary updates.
to learn more about virtual dom and Reconcilisation in react you can watch this video
Conclusion
This blog serves as a collection of my notes from the course "The Hard Parts of UI Development" by Will Sentence. If you're interested in delving deeper into these topics, I highly recommend checking out the course for a more comprehensive understanding.
Thank you for taking the time to read this blog. Most of the information presented here is derived from the course material. If you notice any inaccuracies or have suggestions for improvement, please feel free to let me know, and I will update the blog accordingly.
Subscribe to my newsletter
Read articles from Suhas Khobragade directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Suhas Khobragade
Suhas Khobragade
Frontend Developer, Tech Enthusiast.