Speeding up the Virtual DOM with Vue.js

0
22
Speeding up the Virtual DOM with Vue.js


The concept of a virtual Document Object Model (DOM) was first introduced by the JavaScript framework React in 2013 and is still used today by both React and other frameworks like Vue.js. The idea is to keep an abstract representation of the website structure in memory to reduce the required number of DOM manipulations, while making it easier to detect changes. In practice, this requires two steps: the source code must be parsed to create a virtual DOM tree, and for any change to the data that requires the component to update its appearance, both the virtual DOM and the DOM must be updated.

Advertisement





Timo Zander has studied applied mathematics and computer science and works as a software developer. His interests are in open source, the JavaScript universe and emerging technologies.

These updates can quickly become extremely complex. The framework must create a new virtual DOM tree based on the new data to compare it with the previous data on which the currently displayed HTML elements and components are based. In a naive implementation, the framework can simply recreate the entire DOM (via document.innerHTML = "…") each time. However, this would be extremely slow, especially for large websites. Instead, virtual DOM based frameworks attempt to find specific differences between the two versions of the virtual DOM, often called diffing or reconciliation.

Consider the React app shown in Figure 1. Every second, the time display must be re-rendered to reflect the changed time. And every time the username input is changed, this part of the app must also be re-rendered to reflect the changed input value. In this case, the diff process is quite simple: React must traverse the virtual DOM tree and note the state difference of its two leaf components UsernameInput And Time. As a result, it manipulates only these two small parts of the DOM. For larger websites, this process can become more complex. Data flows are more ambiguous and interdependent, state can be reused across multiple components, and the total number of HTML elements increases. All of these situations pose a challenge for any differentiating algorithm, so optimizations and heuristics are necessary to speed up this process and achieve a stable refresh rate. Some of these strategies are implemented in the Vue.js framework.

The virtual DOM tree of a simple example app (Figure 1).

(Image: Timo Zander)

Vue.js implements a more precise way of handling virtual DOM updates. Due to its single-file components (SFC), the framework has always relied on a compilation step that turns Vue.js code into standard JavaScript. But in addition to transforming the syntax, the compiler also analyzes the code and leaves information about its structure that the runtime can later use to run more efficiently. This approach of a “compiler-informed virtual DOM” allows Vue.js’ rendering performance to regularly outperform React In benchmarking tests. For example, when Vue.js finds pieces of static HTML code within the virtual DOM tree, the compiler spits them out in the render function (Listing 1). This means that instead of recreating these virtual nodes every time the site changes, the runtime reuses the elements initially created for each render. The runtime allocates objects only once — thereby removing the need for garbage collection of unused objects after each render — and it can distinguish elements using referential equality.



// Compiled:
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "foo", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, "bar", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, (
    _hoisted_1,
    _createCommentVNode(" hoisted "),
    _hoisted_2,
    _createCommentVNode(" hoisted "),
    _createElementVNode("div", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
  )))
}

Listing 1: The compiled render function does not recreate virtual nodes for static HTML elements.

Vue.js patch flags Another central mechanism are patch flags. They are labels that the compiler assigns to dynamic nodes that inform the runtime how these nodes can change: for example, some nodes can only change their style or update an attribute value, while others have a completely dynamic internal structure. When executing code, the runtime needs to perform only specific updates and checks according to the node’s patch flags.

To speed up comparisons, flags are expressed as bitmasks (Listing 2) that are compared using bitwise AND Operator. Patch flags do not cover every possible way in which a component can change, but are intended to optimize the most common cases. For example, nodes that contain only dynamic text content (such as dynamic div In Listing 1) receives a flag that tells the compiler to compare the children of the node (which are strings) eqeqeq operators, and replace them if necessary.

export enum PatchFlags {
  TEXT = 1,       // 00000000000000000000000000000001
  CLASS = 1 << 1, // 00000000000000000000000000000010
  STYLE = 1 << 2, // 00000000000000000000000000000100
  PROPS = 1 << 3, // 00000000000000000000000000001000

  // ... more flags
}

Listing 2: Patch flags are stored as bitmasks to improve comparison performance.

Dynamic HTML attributes also receive a specific flag: Both the class and style attribute receive a special flag as well, due to their common appearance and the fact that Vue.js supports Passing JavaScript objects in these props. The compiler normalizes all inputs for these properties into object format, before creating the render function. For other props, Vue.js distinguishes between regular dynamic props—which receive PROPS Flags—and props where not only the values ​​but also the props themselves are dynamic, represented by FULL_PROPS Flag (List 3).

// Static props
 >

// Static props with dynamic value
 >


// Dynamic props
const post = {
  id: 1,
  title: 'My Journey with Vue'
}

Listing 3: Props can either be static with dynamic values, or completely dynamic.

When Vue.js discovers that the properties themselves do not change but only their values ​​change, it can speed up the diff by comparing the values ​​of the old and new components for these props. However, dynamic props require the runtime to compare all existing props in both components. Most frontend frameworks including React, Svelte, and Vue.js recommend users to specify an item key when rendering a list of objects. With the key, the runtime can efficiently identify the same node before and after re-rendering to apply potential changes. Also, it can quickly find deleted or added nodes due to keys not existing in the new or old state, respectively (Figure 2). Since these keys are used to uniquely identify a node within a dynamic list, they must be static and unique.


Keys help Vue.js convert runtime differences into dynamically rendered lists more efficiently (Figure 2).

Keys help Vue.js convert runtime differences into dynamically rendered lists more efficiently (Figure 2).

Keys help Vue.js convert runtime differences into dynamically rendered lists more efficiently (Figure 2).

(Image: Timo Zander)

the element on which "v-for" Returns if the expression is present "KEYED_FRAGMENT" Or "UNKEYED_FRAGMENT" The flags depend on whether the keys are specified or not. This enables the runtime to choose a more performant algorithm if it knows it can at least partially rely on the keys being present. Additionally, HOISTED And BAIL There are special patch flags which, if present, are always a single flag of an element. In the case of HOISTED flag, the runtime can skip the component subtree entirely, since it tags static content that never needs to be hydrated, and BAIL Flag indicating that a component should be processed using a non-optimized, brute force different algorithm.

The nodes that trigger such bail-outs are usually created when users manually write render functions rather than relying on Vue.js’s template compiler, since manual render functions have no patch flags. When a Vue.js component has multiple root nodes, the compiler automatically groups them into “fragments”. Similar to fragments in React. They also receive specific flags depending on their structure and use. In most cases, the order of their children will never change – however, if it does, they receive this "STABLE_FRAGMENT" Hint, so that the runtime can skip any item order checking.

iX Workshop: Switching from classic threads to virtual threads in JavaiX Workshop: Switching from classic threads to virtual threads in Java

LEAVE A REPLY

Please enter your comment!
Please enter your name here