DEV Community

Cover image for How Components are Rendered in a Virtual DOM and How to Optimize Re-Rendering
Biswas Prasana Swain
Biswas Prasana Swain

Posted on

How Components are Rendered in a Virtual DOM and How to Optimize Re-Rendering

When building modern web applications, efficiently updating the UI (user interface) is essential to keeping apps fast and responsive. A common strategy used in many frameworks (like React) is to use a Virtual DOM and components. This article will explain how components are rendered using a Virtual DOM and how we can optimize re-rendering so that the web app doesn’t become slow.

1. What is a Virtual DOM?

The DOM (Document Object Model) is a tree-like structure that represents all the elements on a webpage. Every time you interact with a webpage—clicking buttons, typing text—the browser has to update the DOM, which can be slow.

A Virtual DOM is like a copy of the real DOM but lives only in memory. Instead of updating the real DOM directly every time something changes, we update the Virtual DOM first. Once changes are made, the Virtual DOM compares itself to the old version, finds the differences (this is called diffing), and updates only the parts of the real DOM that need changing.

2. What are Components?

In a modern web app, components are the building blocks of the UI. Think of them as small, reusable parts of a webpage. For example:

  • A button can be a component.
  • A header can be a component.
  • A list of items can be a component.

Each component describes what part of the UI should look like. A component function returns a Virtual DOM tree that represents that UI.

3. Example: Creating a Button Component

Let’s create a simple Button component using pseudocode. This component will return a button with text and a function that runs when the button is clicked.

// Component to display a button
function Button(props) {
    // The Button component returns a Virtual DOM node for a <button> element
    return new VirtualNode("button", { onClick: props.onClick }, [props.text])
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • The Button component takes in props (properties), like the text for the button and an event handler for when it’s clicked.
  • It returns a Virtual DOM node that represents a <button> element with the provided text and onClick event handler.

4. Rendering Multiple Components

Let’s say we want to build an app that has a header and a button. Each of these parts can be represented as components. The app’s structure could look like this:

// App component with a header and button
function App() {
    return new VirtualNode("div", {}, [
        new Header(), // The Header component
        new Button({ text: "Click Me", onClick: handleClick }) // The Button component
    ])
}

// Header component
function Header() {
    return new VirtualNode("h1", {}, ["Welcome to the App!"])
}

// Function to handle button clicks
function handleClick() {
    console.log("Button clicked!")
}
Enter fullscreen mode Exit fullscreen mode
  • The App component returns a Virtual DOM tree containing two components: Header and Button.
  • The Header component returns a Virtual DOM node representing an <h1> element.
  • The Button component works as we described earlier.

5. How Initial Rendering Works

When the app runs for the first time, it:

  1. Calls the components: App(), Header(), and Button() are executed.
  2. Creates the Virtual DOM: The result is a tree of Virtual DOM nodes that represents the UI.
  3. Updates the real DOM: The Virtual DOM is used to build the actual HTML elements in the real DOM.
// Initial render of the app
function renderApp() {
    let virtualDOM = App()          // Render the app's Virtual DOM
    let realDOM = createRealDOM(virtualDOM)  // Convert the Virtual DOM into real DOM elements
    attachToPage(realDOM)           // Attach the real DOM elements to the webpage
}
Enter fullscreen mode Exit fullscreen mode

6. Re-Rendering and Why We Need Optimization

Let’s say that something in the app changes, like the button text. Normally, the entire app would be re-rendered, but this can be slow if the app is large. Instead, we can optimize re-rendering by updating only the parts that changed.

Here’s what happens when re-rendering occurs:

  1. Diffing: We compare the old Virtual DOM with the new one and figure out what has changed.
  2. Patching: Only the parts of the real DOM that need to be updated are changed (this process is called patching).

Example: Changing the Button Text

Let’s say the button text changes from "Click Me" to "Clicked!". Here’s how we would re-render the button:

// New Button component with updated text
function Button(props) {
    return new VirtualNode("button", { onClick: props.onClick }, [props.text])
}

// Re-rendering with the new text
let oldButton = Button({ text: "Click Me", onClick: handleClick })
let newButton = Button({ text: "Clicked!", onClick: handleClick })

// Diff the old and new Button
let diffResult = diff(oldButton, newButton)

// Patch the real DOM with the changes
patch(realButtonDOM, diffResult)
Enter fullscreen mode Exit fullscreen mode

7. Optimizing Re-Rendering: Should Component Update

One of the key ways to optimize re-rendering is by checking whether a component actually needs to update. If nothing has changed in the component’s props or state, we can skip re-rendering that component. This is where the shouldComponentUpdate logic comes in.

// Function to check if a component should update
function shouldComponentUpdate(oldProps, newProps) {
    return oldProps !== newProps // Only update if the props have changed
}
Enter fullscreen mode Exit fullscreen mode

Now, before re-rendering, we check if the component should update:

// Example: Optimized re-rendering of Button component
function renderButtonIfNeeded(oldButton, newButton) {
    if (shouldComponentUpdate(oldButton.props, newButton.props)) {
        let realButton = createRealDOM(newButton)
        patch(realButton)
    }
}
Enter fullscreen mode Exit fullscreen mode

8. Using Keys for List Optimization

When rendering lists of items (e.g., a list of buttons), we can optimize by using keys to uniquely identify each item. This helps the diffing algorithm match up old and new items in the list and apply only the necessary changes.

// List of buttons with unique keys
function ButtonList(items) {
    return new VirtualNode("div", {}, items.map(item => 
        new Button({ key: item.id, text: item.text, onClick: handleClick })
    ))
}
Enter fullscreen mode Exit fullscreen mode

With keys, if one of the items in the list changes (like adding or removing a button), the algorithm can quickly identify which button changed and update only that one.

9. Optimizing State Changes

Components can also have their own state. When the state of a component changes, we only want to re-render that specific component, not the whole app. Here’s an example of a button with state:

// Button component with state
function ButtonWithState() {
    let [clicked, setClicked] = useState(false) // Create state for button

    function handleClick() {
        setClicked(true) // Update state when clicked
    }

    return new VirtualNode("button", { onClick: handleClick }, [clicked ? "Clicked!" : "Click Me"])
}
Enter fullscreen mode Exit fullscreen mode

In this case:

  • The button text changes when it’s clicked.
  • Only the ButtonWithState component re-renders, and the real DOM only updates the button text.

10. Avoid Re-Rendering Parent Components

Another optimization is to avoid re-rendering parent components when only a child component changes. For example, if the button changes but the Header stays the same, we can skip re-rendering the Header.

// Optimized App component
function App() {
    if (!shouldComponentUpdate(oldHeaderProps, newHeaderProps)) {
        return oldHeader // Reuse the old Header if it hasn't changed
    }

    return new VirtualNode("div", {}, [
        new Header(), // Re-render the Header only if necessary
        new ButtonWithState() // Button re-renders based on state
    ])
}
Enter fullscreen mode Exit fullscreen mode

11. Conclusion: Efficient UI Updates with a Virtual DOM

To summarize, we can break down the process of rendering and optimizing components using the Virtual DOM into these steps:

  1. Initial Rendering: The first time the app is rendered, we build the Virtual DOM tree and convert it to the real DOM.
  2. Re-Rendering: When something changes (like the button text or state), we update the Virtual DOM and apply only the necessary changes to the real DOM.
  3. Optimizing Re-Renders: By using strategies like shouldComponentUpdate, keys for lists, and state-based updates, we can avoid unnecessary re-rendering, keeping the app fast and responsive.

By thinking carefully about when and what to re-render, we can make sure that web applications remain efficient even as they grow in complexity. The Virtual DOM is a powerful tool that helps achieve this balance between simplicity and performance!

Top comments (0)