background

In the last article, we analyzed vue.js 3.2’s optimization of the responsive part. In addition, in this optimization upgrade, there is also a runtime optimization:

~200% faster creation of plain element VNodes

The creation of vNodes for common element types improves performance by about 200%. This is also a great optimization, implemented by Hysunyang, Vue’s official core developer. Check out the PR.

So how does this work? Before I analyze the implementation, I want to give you some background on VNode.

What is a vnode

A VNode is essentially a JavaScript object that describes the DOM. It can describe different types of nodes in vue.js, such as common element nodes, component nodes, and so on.

Common element vNode

What are ordinary element nodes? For example, in HTML we use the

<button class="btn" style="width:100px; height:50px">click me</button>
Copy the code

We can use vnode to represent the

const vnode = {
  type: 'button'.props: { 
    'class': 'btn'.style: {
      width: '100px'.height: '50px'}},children: 'click me'
}
Copy the code

The type attribute represents the label type of the DOM. The props attribute represents additional information about the DOM, such as style, class, and so on. The children property represents a child node of the DOM, which in this case is a simple text string, or, of course, children could be a VNode array.

Component vnode

In addition to describing a real DOM as described above, vNode can also be used to describe components. For example, we introduce a component tag < custom-Component > in the template:

<custom-component msg="test"></custom-component>
Copy the code

We can represent the

component tag with vnode as follows:

const CustomComponent = {
  // Define the component object here
}
const vnode = {
  type: CustomComponent,
  props: { 
    msg: 'test'}}Copy the code

Component VNode is really a description of something abstract, because we don’t actually render a < custom-Component > tag on the page. We end up rendering the HTML tag defined inside the component.

In addition to the two vNode types mentioned above, there are plain text vNodes, comment vnodes, and so on.

In addition, vue.js 3.x also makes more detailed classification for vnode types, including Suspense, Teleport, etc., and codes vnode type information so that in the later vnode mount stage, The corresponding processing logic can be executed according to different types:

// runtime-core/src/vnode.ts
const shapeFlag = isString(type)
  ? 1 /* ELEMENT */
  : isSuspense(type)
    ? 128 /* SUSPENSE */
    : isTeleport(type)
      ? 64 /* TELEPORT */
      : isObject(type)
        ? 4 /* STATEFUL_COMPONENT */
        : isFunction(type)
          ? 2 /* FUNCTIONAL_COMPONENT */
          : 0;
Copy the code

The advantage of the vnode

Now that you know what a VNode is, you may be wondering, what are the advantages of a VNode? Why design a data structure like VNode?

The first is abstraction. With the introduction of VNode, the rendering process can be abstracted, thus improving the abstraction ability of components.

Secondly, cross-platform, because the patch vnode process of different platforms can have their own implementation, based on VNode to do server rendering, WEEX platform, small program platform rendering have become a lot easier.

It’s important to note, however, that using vNode in the browser doesn’t mean you don’t have to manipulate the DOM. Many people mistakenly assume that vNode will perform better than the native DOM manually, which is not always the case.

Because of the MVVM framework based on the VNode implementation, there is a certain amount of JavaScript time required for each component rendering to generate a VNode, especially for large components. For example, for a 1000 * 10 Table component, the component rendering process to generate vnodes will traverse 1000 * 10 times to create internal cell vnodes, which will take a long time. In addition, the process of mounting Vnodes to generate DOM will also take a certain amount of time. When we went to update the component, the user felt a noticeable lag.

While the Diff algorithm is good enough to minimize DOM manipulation, ultimately DOM manipulation is unavoidable, so performance is not an advantage of VNode.

How do I create a VNode

We usually develop components by writing component templates and do not write vNodes by hand. How is a VNode created?

As we know, the component template is compiled to generate the corresponding render function. Inside the render function, the createVNode function is executed to create the vNode object.

function createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
  if(! type || type === NULL_DYNAMIC_COMPONENT) {if((process.env.NODE_ENV ! = ='production') && !type) {
      warn(`Invalid vnode type when creating vnode: ${type}. `)
    }
    type = Comment
  }
  if (isVNode(type)) {
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    return cloned
  }
  // Class component standardization
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }
  // Class and style are standardized.
  if (props) {
    if (isProxy(props) || InternalObjectKey in props) {
      props = extend({}, props)
    }
    let { class: klass, style } = props
    if(klass && ! isString(klass)) { props.class = normalizeClass(klass) }if (isObject(style)) {
      if(isProxy(style) && ! isArray(style)) { style = extend({}, style) } props.style = normalizeStyle(style) } }// Encode according to the vNode type
  const shapeFlag = isString(type)
    ? 1 /* ELEMENT */
    : isSuspense(type)
      ? 128 /* SUSPENSE */
      : isTeleport(type)
        ? 64 /* TELEPORT */
        : isObject(type)
          ? 4 /* STATEFUL_COMPONENT */
          : isFunction(type)
            ? 2 /* FUNCTIONAL_COMPONENT */
            : 0
  if((process.env.NODE_ENV ! = ='production') && shapeFlag & 4 /* STATEFUL_COMPONENT */ && isProxy(type)) {
    type = toRaw(type)
    warn(`Vue received a Component which was made a reactive object. This can ` +
      `lead to unnecessary performance overhead, and should be avoided by ` +
      `marking the component with `markRaw` or using `shallowRef` ` +
      `instead of `ref`. `.`\nComponent that was made reactive: `, type)
  }
  const vnode = {
    __v_isVNode: true.__v_skip: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null.children: null.component: null.suspense: null.ssContent: null.ssFallback: null.dirs: null.transition: null.el: null.anchor: null.target: null.targetAnchor: null.staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null.appContext: null
  }
  if((process.env.NODE_ENV ! = ='production') && vnode.key ! == vnode.key) { warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
  }
  normalizeChildren(vnode, children)
  // Standardize suspense child nodes
  if (shapeFlag & 128 /* SUSPENSE */) {
    type.normalize(vnode)
  }
  if (isBlockTreeEnabled > 0 &&
    !isBlockNode &&
    currentBlock &&
    (patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) && patchFlag ! = =32 /* HYDRATE_EVENTS */) {
    currentBlock.push(vnode)
  }
  return vnode
}
Copy the code

As you can see, the vNode creation process does a lot of things, including a lot of logic to determine whether type is null:

if(! type || type === NULL_DYNAMIC_COMPONENT) {if((process.env.NODE_ENV ! = ='production') && !type) {
    warn(`Invalid vnode type when creating vnode: ${type}. `)
  }
  type = Comment
}
Copy the code

Check whether type is a vNode:

if (isVNode(type)) {
  const cloned = cloneVNode(type, props, true /* mergeRef: true */)
  if (children) {
    normalizeChildren(cloned, children)
  }
  return cloned
}
Copy the code

Check whether type is a component of class type:

if (isClassComponent(type)) {
    type = type.__vccOpts
  }
Copy the code

In addition, the style and class attributes are standardized, and there is some judgment logic:

if (props) {
  if (isProxy(props) || InternalObjectKey in props) {
    props = extend({}, props)
  }
  let { class: klass, style } = props
  if(klass && ! isString(klass)) { props.class = normalizeClass(klass) }if (isObject(style)) {
    if(isProxy(style) && ! isArray(style)) { style = extend({}, style) } props.style = normalizeStyle(style) } }Copy the code

Next, the vNode is encoded according to its type:

const shapeFlag = isString(type)
  ? 1 /* ELEMENT */
  : isSuspense(type)
    ? 128 /* SUSPENSE */
    : isTeleport(type)
      ? 64 /* TELEPORT */
      : isObject(type)
        ? 4 /* STATEFUL_COMPONENT */
        : isFunction(type)
          ? 2 /* FUNCTIONAL_COMPONENT */
          : 0
Copy the code

After creating the vNode object, normalizeChildren is executed to normalize the child nodes. This process also has a set of judgment logic.

Optimization of the vNode creation process

If you think about it, a VNode is essentially a JavaScript object, and the reason a lot of judgment is made during the creation process is because you’re dealing with a variety of situations. However, for a normal vnode element, there is no need for so much logic, so using the createVNode function to create a normal vnode element is wasteful.

With this in mind, you can create a vNode with a new function for a common element node during template compilation. This is what vue.js 3.2 does, for example:

<template>
  <div class="home">
    <img alt="Vue logo" src=".. /assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>
Copy the code

With the help of the template export tool, you can see its compiled render function:

import { createElementVNode as _createElementVNode, resolveComponent as _resolveComponent, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = { class: "home" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("img", {
  alt: "Vue logo".src: ".. /assets/logo.png"
}, null, -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_HelloWorld = _resolveComponent("HelloWorld")

  return (_openBlock(), _createElementBlock("template".null, [
    _createElementVNode("div", _hoisted_1, [
      _hoisted_2,
      _createVNode(_component_HelloWorld, { msg: "Welcome to Your Vue.js App"}})]]))Copy the code

For div nodes, the createElementVNode method is used instead of the createVNode method. Internally, createElementVNode is the alias of createBaseVNode.

function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : 1 /* ELEMENT */, isBlockNode = false, needFullChildrenNormalization = false) {
  const vnode = {
    __v_isVNode: true.__v_skip: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null,
    children,
    component: null.suspense: null.ssContent: null.ssFallback: null.dirs: null.transition: null.el: null.anchor: null.target: null.targetAnchor: null.staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null.appContext: null
  }
  if (needFullChildrenNormalization) {
    normalizeChildren(vnode, children)
    if (shapeFlag & 128 /* SUSPENSE */) {
      type.normalize(vnode)
    }
  }
  else if (children) {
    vnode.shapeFlag |= isString(children)
      ? 8 /* TEXT_CHILDREN */
      : 16 /* ARRAY_CHILDREN */
  }
  if((process.env.NODE_ENV ! = ='production') && vnode.key ! == vnode.key) { warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
  }
  if (isBlockTreeEnabled > 0 &&
    !isBlockNode &&
    currentBlock &&
    (vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) && vnode.patchFlag ! = =32 /* HYDRATE_EVENTS */) {
    currentBlock.push(vnode)
  }
  return vnode
}
Copy the code

As you can see, createBaseVNode simply creates the vNode object and then does some block logic. CreateBaseVNode performs much less judgment logic than the previous createVNode implementation, resulting in a natural performance improvement.

The createVNode implementation is a layer of encapsulation based on createBaseVNode:

function createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
  if(! type || type === NULL_DYNAMIC_COMPONENT) {if((process.env.NODE_ENV ! = ='production') && !type) {
      warn(`Invalid vnode type when creating vnode: ${type}. `)
    }
    type = Comment$1
  }
  if (isVNode(type)) {
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    return cloned
  }
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }
  if (props) {
    props = guardReactiveProps(props)
    let { class: klass, style } = props
    if(klass && ! isString(klass)) { props.class = normalizeClass(klass) }if (isObject$1(style)) {
      if(isProxy(style) && ! isArray(style)) { style = extend({}, style) } props.style = normalizeStyle(style) } }const shapeFlag = isString(type)
    ? 1 /* ELEMENT */
    : isSuspense(type)
      ? 128 /* SUSPENSE */
      : isTeleport(type)
        ? 64 /* TELEPORT */
        : isObject$1(type)
          ? 4 /* STATEFUL_COMPONENT */
          : isFunction$1(type)
            ? 2 /* FUNCTIONAL_COMPONENT */
            : 0
  if((process.env.NODE_ENV ! = ='production') && shapeFlag & 4 /* STATEFUL_COMPONENT */ && isProxy(type)) {
    type = toRaw(type)
    warn(`Vue received a Component which was made a reactive object. This can ` +
      `lead to unnecessary performance overhead, and should be avoided by ` +
      `marking the component with `markRaw` or using `shallowRef` ` +
      `instead of `ref`. `.`\nComponent that was made reactive: `, type)
  }
  return createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true)}Copy the code

The createVNode implementation is similar to the previous one. A bunch of logic is required to create a vNode by executing createBaseVNode. Note that the createBaseVNode parameter is passed to true. Namely needFullChildrenNormalization is true, then in the interior of the createBaseVNode, need to perform more normalizeChildren logic.

The component vNode is also created using the createVNode function.

conclusion

It may seem like only a few lines of code are missing, but since most pages are made up of a lot of common DOM elements, optimizing the process of creating common element VNodes can make a big difference in overall page rendering and updating.

Since there is a template compilation process, vue.js can use compilation + runtime optimization to achieve overall performance optimization. The Block Tree design, for example, optimizes the performance of diff procedures.

In fact, the more you get to know a framework, the more in awe you will feel. Vue.js takes a lot of effort in compiling and implementing it at runtime, dealing with a lot of details, so the size of the code will inevitably get bigger. And it is not easy to do so many performance optimizations internally, without regression bugs, when the framework is mature enough to be used by a large number of users.

The more users of open source works, the greater the challenge will be, the more details need to be considered, if an open source works are not used, toy level, really don’t touch porcelain Vue, it is not a segment.