Skip to content

Patch Flags

What are Patch Flags?

Patch Flags are optimization hints generated by the compiler. By attaching flags to VNodes, the runtime's diffing algorithm can skip unnecessary checks and improve performance.

Kawaiko mascot - question
Why does the compiler optimize?

When humans write templates, they understand "this part is dynamic" and "this part is static", but traditional Virtual DOM doesn't know this. By having the compiler convey this information to the runtime, unnecessary comparisons can be eliminated!

How the Optimization Works

In normal Virtual DOM diffing, all properties and child elements need to be compared. However, the compiler knows "which parts are dynamic" at the template analysis stage. By embedding this information as Patch Flags in VNodes, the runtime can check only the parts that might change.

PatchFlags Definition

ts
export const enum PatchFlags {
  /**
   * Element with dynamic textContent
   */
  TEXT = 1,

  /**
   * Element with dynamic class binding
   */
  CLASS = 1 << 1,  // 2

  /**
   * Element with dynamic style
   */
  STYLE = 1 << 2,  // 4

  /**
   * Element with dynamic props other than class/style
   */
  PROPS = 1 << 3,  // 8

  /**
   * Element with props that have dynamic keys
   */
  FULL_PROPS = 1 << 4,  // 16

  /**
   * Props processing needed during hydration
   */
  NEED_HYDRATION = 1 << 5,  // 32

  /**
   * Fragment with children whose order doesn't change
   */
  STABLE_FRAGMENT = 1 << 6,  // 64

  /**
   * Fragment with keyed children
   */
  KEYED_FRAGMENT = 1 << 7,  // 128

  /**
   * Fragment with unkeyed children
   */
  UNKEYED_FRAGMENT = 1 << 8,  // 256

  /**
   * Patch needed for non-props (ref, directives, etc.)
   */
  NEED_PATCH = 1 << 9,  // 512

  /**
   * Component with dynamic slots
   */
  DYNAMIC_SLOTS = 1 << 10,  // 1024

  /**
   * For development: Fragment with comment at root
   */
  DEV_ROOT_FRAGMENT = 1 << 11,  // 2048

  // Special flags (negative integers)

  /**
   * Cached static VNode
   */
  CACHED = -1,

  /**
   * Hint to exit optimized mode
   */
  BAIL = -2,
}

Combining with Bitwise Operations

Patch Flags are designed as bit flags, allowing multiple flags to be combined.

ts
// Combining flags
const flag = PatchFlags.TEXT | PatchFlags.CLASS;  // 3 (0b11)

// Checking flags
if (flag & PatchFlags.TEXT) {
  // TEXT flag is set
}

if (flag & PatchFlags.CLASS) {
  // CLASS flag is set
}
Kawaiko mascot - funny
The magic of bitwise operations

1 << 1 is 2, 1 << 2 is 4... just shifting bits creates independent flags. Combine with | (OR), check with & (AND). Simple but super efficient!

Examples of Generation from Templates

Dynamic Text

vue
<template>
  <p>{{ message }}</p>
</template>

Generated code:

js
// patchFlag = 1 (TEXT)
createVNode("p", null, toDisplayString(message), 1 /* TEXT */)

Dynamic Class

vue
<template>
  <div :class="dynamicClass">Content</div>
</template>

Generated code:

js
// patchFlag = 2 (CLASS)
createVNode("div", { class: dynamicClass }, "Content", 2 /* CLASS */)

Multiple Dynamic Properties

vue
<template>
  <div :class="cls" :style="styles">{{ text }}</div>
</template>

Generated code:

js
// patchFlag = 7 (TEXT | CLASS | STYLE)
createVNode("div",
  { class: cls, style: styles },
  toDisplayString(text),
  7 /* TEXT, CLASS, STYLE */
)

Dynamic Props

vue
<template>
  <input :value="inputValue" :disabled="isDisabled">
</template>

Generated code:

js
// patchFlag = 8 (PROPS)
// dynamicProps explicitly specifies props that may change
createVNode("input",
  { value: inputValue, disabled: isDisabled },
  null,
  8 /* PROPS */,
  ["value", "disabled"]
)

Usage in Runtime

Optimization in patchElement

ts
function patchElement(n1: VNode, n2: VNode) {
  const el = n2.el = n1.el;
  const { patchFlag, dynamicProps } = n2;

  if (patchFlag > 0) {
    // Optimized path: update only necessary parts based on flags

    if (patchFlag & PatchFlags.CLASS) {
      // Update only class
      if (n1.props?.class !== n2.props?.class) {
        hostSetClass(el, n2.props?.class);
      }
    }

    if (patchFlag & PatchFlags.STYLE) {
      // Update only style
      hostPatchStyle(el, n1.props?.style, n2.props?.style);
    }

    if (patchFlag & PatchFlags.PROPS) {
      // Update only specified props
      for (const key of dynamicProps!) {
        const prev = n1.props?.[key];
        const next = n2.props?.[key];
        if (prev !== next) {
          hostPatchProp(el, key, prev, next);
        }
      }
    }

    if (patchFlag & PatchFlags.TEXT) {
      // Update only text content
      if (n1.children !== n2.children) {
        hostSetElementText(el, n2.children as string);
      }
    }
  } else if (patchFlag === PatchFlags.FULL_PROPS) {
    // Check all props
    patchProps(el, n1.props, n2.props);
  } else {
    // No flags: full diff
    patchProps(el, n1.props, n2.props);
    patchChildren(n1, n2, el);
  }
}

Fragment Optimization

ts
function patchFragment(n1: VNode, n2: VNode) {
  const { patchFlag } = n2;

  if (patchFlag & PatchFlags.STABLE_FRAGMENT) {
    // Children order doesn't change: simple update
    patchBlockChildren(n1.children, n2.children);
  } else if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
    // Keyed children: key-based diff
    patchKeyedChildren(n1.children, n2.children);
  } else {
    // Unkeyed: full diff
    patchUnkeyedChildren(n1.children, n2.children);
  }
}

Special Flags

CACHED (-1)

Indicates that a static VNode is cached.

js
const _hoisted_1 = createVNode("div", null, "Static", -1 /* CACHED */);

Cached VNodes can skip diffing.

BAIL (-2)

A hint to exit optimized mode. Used when the compiler's optimizations cannot be applied, such as when the user is using hand-written render functions.

dynamicProps

The dynamicProps array, used together with patchFlag, explicitly indicates which props are dynamic.

ts
// Dynamic props are value and disabled
createVNode("input",
  { type: "text", value: val, disabled: isDisabled },
  null,
  8 /* PROPS */,
  ["value", "disabled"]  // dynamicProps
)

This allows skipping comparison for type since it's static, and only checking value and disabled.

Integration with Block Tree

Patch Flags work in conjunction with Block Tree optimization. Blocks have a dynamicChildren array that tracks only dynamic child nodes.

ts
const block = openBlock();
const vnode = createBlock("div", null, [
  createVNode("p", null, "static"),  // not included in dynamicChildren
  createVNode("p", null, toDisplayString(msg), 1 /* TEXT */)  // included
]);
// block.dynamicChildren = [only the dynamic p]

When updating a Block, only dynamicChildren needs to be traversed, allowing comparison of static child nodes to be skipped.

Effect of Optimization

Before (without flags)

Compare all props: O(n)
Compare all children: O(m)
Total: O(n + m)

After (with flags)

Compare only dynamic props: O(k) where k << n
Compare only dynamic children: O(l) where l << m
Total: O(k + l)

When most of the template is static, this optimization has a significant effect.

Summary

The Patch Flags implementation consists of the following elements:

  1. Bit flags: Efficiently represent multiple dynamic elements
  2. Compiler integration: Automatically generated during template analysis
  3. Runtime optimization: Skip unnecessary comparisons based on flags
  4. dynamicProps: Explicitly track dynamic props
  5. Block Tree integration: Efficiently update only dynamic child nodes

Patch Flags are an important optimization technique that significantly improves Vue 3's Virtual DOM performance. By having the compiler and runtime work together, the advantages of template-based frameworks are maximized.

Kawaiko mascot - surprise
Patch Flags complete!

This technology was born from the idea "If we can analyze templates, we can also provide optimization hints." Please experience the strength of template compilers that JSX doesn't have!

Source code up to this point: chibivue (GitHub)

Released under the MIT License.