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:

  1. 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

  2. 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

  3. 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"

  4. 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:

  1. 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

  2. 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

  3. 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:

  1. JavaScript engine finds the DOM node using C++ bindings

  2. Updates the node's properties - this marks the DOM as changed

  3. The layout and rendering engine detects DOM changes and creates new bitmap images

  4. 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:

  1. The input event fires

  2. Our handler function executes

  3. The DOM updates

  4. 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:

  1. Centralized DOM updates

  2. Predictable data flow

  3. Easier debugging

  4. 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.

0
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.