Chapter 14

VNode Internals and ShapeFlags Bitwise Operations

Chapter 14: VNode Internal Structure and ShapeFlags Bitmasking

Vue 3's VNode object has on average 12 fields, yet the single shapeFlag integer carries type information that would otherwise require 10 separate boolean fields โ€” this isn't showing off, it's an intentional design to exploit V8's inline cache fast path for small objects.

Core question of this chapter: What exactly is a VNode's structure? How does the renderer determine in microseconds whether a node is a component, a native element, or plain text?

After reading this chapter you will understand:

Level 1 ยท What You Need to Know (1โ€“3 Years Experience)

What Is a VNode: A Plain JS Object Describing UI Intent

A Virtual Node (VNode) is not a DOM node, and it is not a component instance. It is a "blueprint" that describes "what kind of DOM node or component I want to have." The renderer reads this blueprint and decides how to manipulate the real DOM.

The most important fact: a VNode object lives for exactly one render frame. Every render cycle, Vue generates a brand-new VNode tree, then diffs it against the previous frame's tree, applying only the differences to the real DOM.

The 11 Core Fields of a VNode

interface VNode {
  // Type identifier (the most important field)
  type: string | object | symbol | Function

  // Unique key for diff-based reuse
  key: string | number | symbol | null

  // Template ref binding
  ref: VNodeNormalizedRef | null

  // Props passed to the element or component
  props: Record<string, any> | null

  // Child nodes: string, array, or slot object
  children: VNodeNormalizedChildren

  // After mounting: points to the component instance (component VNodes only)
  component: ComponentInternalInstance | null

  // After mounting: points to the corresponding real DOM node (element VNodes only)
  el: HostElement | null

  // Type bitmask: encodes all type characteristics in a single integer
  shapeFlag: number

  // Compiler-generated update hint (0 = full diff, >0 = targeted update)
  patchFlag: number

  // List of dynamically compared prop names (only valid when patchFlag includes FULL_PROPS)
  dynamicProps: string[] | null

  // Dynamic child nodes collected by the Block tree (key to skipping static nodes)
  dynamicChildren: VNode[] | null
}

The Four Forms of the type Field

type is the renderer's first basis for deciding how to handle a VNode:

type value Node type Renderer behavior
'div', 'span', 'input', etc. Native HTML element Create/update DOM element
{ setup, render, ... } object Stateful component Create component instance, call setup
(props, ctx) => VNode function Functional component Call function directly to get VNode
Symbol(Fragment), Symbol(Text), etc. Special container nodes Each has its own special handling
// String type: native element
const divVNode = h('div', { class: 'box' }, 'Hello')
// divVNode.type === 'div'

// Object type: stateful component
const MyComp = defineComponent({ setup() { return () => h('span', 'Hi') } })
const compVNode = h(MyComp, { title: 'test' })
// compVNode.type === MyComp

// Symbol type: Fragment (multi-root)
const fragVNode = h(Fragment, null, [h('li', 'a'), h('li', 'b')])
// fragVNode.type === Fragment (a Symbol)

Three Special Symbol Types

// packages/runtime-core/src/vnode.ts
export const Fragment = Symbol('Fragment')
export const Text    = Symbol('Text')
export const Comment = Symbol('Comment')
export const Static  = Symbol('Static')

Intuitive Understanding of the shapeFlag Field

Suppose you want to store the information "is this node a stateful component?" The most obvious way is a boolean:

vnode.isStatefulComponent = true
vnode.hasArrayChildren = false
vnode.hasSlotsChildren = true
// ... plus 8 more booleans

But Vue 3 expresses the same information with one number:

vnode.shapeFlag = 36  // binary 100100 = stateful component (4) | slot children (32)

The benefits: memory reduced from 10 fields to 1, and type checking goes from property access to a bitwise operation (faster).

Where You Encounter VNodes in Day-to-Day Code

<script setup>
import { h, cloneVNode, useSlots } from 'vue'

const slots = useSlots()

// Access the VNodes in a slot
const defaultSlotVNodes = slots.default?.()

// Clone and modify a VNode (useful in higher-order components)
if (defaultSlotVNodes?.length) {
  const cloned = cloneVNode(defaultSlotVNodes[0], { class: 'extra-class' })
}
</script>

Level 2 ยท How It Actually Works (3โ€“5 Years Experience)

The Complete ShapeFlags Enum

// packages/shared/src/shapeFlags.ts
export const enum ShapeFlags {
  ELEMENT                     = 1,        // 0000_0000_0001
  FUNCTIONAL_COMPONENT        = 1 << 1,   // 0000_0000_0010  = 2
  STATEFUL_COMPONENT          = 1 << 2,   // 0000_0000_0100  = 4
  TEXT_CHILDREN               = 1 << 3,   // 0000_0000_1000  = 8
  ARRAY_CHILDREN              = 1 << 4,   // 0000_0001_0000  = 16
  SLOTS_CHILDREN              = 1 << 5,   // 0000_0010_0000  = 32
  TELEPORT                    = 1 << 6,   // 0000_0100_0000  = 64
  SUSPENSE                    = 1 << 7,   // 0000_1000_0000  = 128
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,   // 0001_0000_0000  = 256
  COMPONENT_KEPT_ALIVE        = 1 << 9,   // 0010_0000_0000  = 512
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT, // = 6
}

Note that COMPONENT = 6, which is the bitwise OR of STATEFUL_COMPONENT(4) | FUNCTIONAL_COMPONENT(2). This means:

// One bitwise operation to match both component types simultaneously
if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
  // This branch handles both stateful and functional components
}

How Bitwise Detection Works Internally

Bitwise operation diagram:

A VNode with shapeFlag = 36 (stateful component + slot children):

Binary representation of shapeFlag:
  Bit 9  8  7  6  5  4  3  2  1  0
       0  0  0  0  1  0  0  1  0  0   = 36

Checking STATEFUL_COMPONENT (4 = 0000_0100):
  shapeFlag:  0000_0010_0100
  mask:       0000_0000_0100   (ShapeFlags.STATEFUL_COMPONENT)
  AND result: 0000_0000_0100   = 4 (nonzero โ†’ true, is a stateful component)

Checking ELEMENT (1 = 0000_0001):
  shapeFlag:  0000_0010_0100
  mask:       0000_0000_0001   (ShapeFlags.ELEMENT)
  AND result: 0000_0000_0000   = 0 (zero โ†’ false, not a native element)

Checking SLOTS_CHILDREN (32 = 0010_0000):
  shapeFlag:  0000_0010_0100
  mask:       0000_0010_0000   (ShapeFlags.SLOTS_CHILDREN)
  AND result: 0000_0010_0000   = 32 (nonzero โ†’ true, has slot children)

The actual renderer patch dispatch code:

// packages/runtime-core/src/renderer.ts (simplified)
function patch(n1, n2, container) {
  const { type, shapeFlag } = n2

  // First dispatch special Symbol types
  switch (type) {
    case Text:
      processText(n1, n2, container)
      break
    case Comment:
      processCommentNode(n1, n2, container)
      break
    case Fragment:
      processFragment(n1, n2, container)
      break
    default:
      // Then dispatch by shapeFlag for regular types
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(n1, n2, container)
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(n1, n2, container)
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        type.process(n1, n2, container, ...)
      } else if (shapeFlag & ShapeFlags.SUSPENSE) {
        type.process(n1, n2, container, ...)
      }
  }
}

The Complete Execution Flow of createVNode

createVNode() execution chain:

createVNode(type, props, children)
    โ”‚
    โ”œโ”€โ”€ 1. Normalize type (handle defineComponent return values)
    โ”‚
    โ”œโ”€โ”€ 2. Compute shapeFlag
    โ”‚       isString(type)          โ†’ ELEMENT(1)
    โ”‚       isSuspense(type)        โ†’ SUSPENSE(128)
    โ”‚       isTeleport(type)        โ†’ TELEPORT(64)
    โ”‚       isFunction(type)        โ†’ FUNCTIONAL_COMPONENT(2)
    โ”‚       isObject(type)          โ†’ STATEFUL_COMPONENT(4)
    โ”‚
    โ”œโ”€โ”€ 3. Normalize props (handle special forms of class/style)
    โ”‚       class: ['a', {b: true}] โ†’ 'a b' (pre-compute string)
    โ”‚       style: [{color:'red'}]  โ†’ {color: 'red'} (merge objects)
    โ”‚
    โ”œโ”€โ”€ 4. Normalize children, update shapeFlag
    โ”‚       string children         โ†’ shapeFlag |= TEXT_CHILDREN(8)
    โ”‚       array children          โ†’ shapeFlag |= ARRAY_CHILDREN(16)
    โ”‚       object children (slots) โ†’ shapeFlag |= SLOTS_CHILDREN(32)
    โ”‚
    โ”œโ”€โ”€ 5. Collect into current Block's dynamicChildren (if patchFlag present)
    โ”‚
    โ””โ”€โ”€ 6. Return VNode object

The Differences Between createVNode, createElementVNode, and h()

These three functions operate at different levels of abstraction:

// h() is the user-facing API with the most argument normalization
h('div', 'text')                    // string as children
h('div', ['a', 'b'])                // array as children
h('div', { class: 'box' }, 'text')  // props + children

// createVNode() is the internal layer โ€” less normalization than h()
// The compiler never generates h(); it generates createVNode() or faster variants

// createElementVNode() is a specialized version of createVNode()
// Only handles native elements (type is known to be a string),
// skips the isObject/isFunction checks.
// The compiler generates createElementVNode() for native elements in templates,
// which is approximately 20% faster than createVNode().

// Actual compiled output:
// <div class="box">Hello</div>
// compiles to:
createElementVNode("div", { class: "box" }, "Hello")
// NOT:
createVNode("div", { class: "box" }, "Hello")

cloneVNode: Usage Scenarios and Implementation

cloneVNode is not a simple shallow object copy โ€” it must handle the merging of multiple VNode fields:

// packages/runtime-core/src/vnode.ts (simplified)
export function cloneVNode(vnode, extraProps?, children?) {
  const { props, ref, patchFlag, children: existingChildren } = vnode

  const mergedProps = extraProps
    ? mergeProps(props || {}, extraProps)
    : props

  return {
    ...vnode,                          // Copy all fields
    props: mergedProps,
    // ref requires special handling: merge multiple refs
    ref: ...,
    // Prefer newly passed children
    children: children ?? existingChildren,
    // Reset patchFlag to -2 (meaning "cloned, needs full diff")
    patchFlag: patchFlag === PatchFlags.HOISTED
      ? PatchFlags.HOISTED
      : patchFlag | PatchFlags.BAIL,
    // The cloned VNode no longer belongs to the original Block
    dynamicChildren: null,
  }
}

Common usage scenarios:

// Scenario 1: HOC adds extra attributes to slot content
const HOC = defineComponent({
  setup(_, { slots }) {
    return () => {
      const children = slots.default?.()
      if (!children) return null
      return children.map(vnode =>
        cloneVNode(vnode, { class: 'hoc-wrapped' })
      )
    }
  }
})

// Scenario 2: TransitionGroup injects keys into child nodes
// Vue's own source code uses cloneVNode extensively for this

Fragment VNode: Mounting and Unmounting

Fragment's special characteristic is that it has no corresponding DOM node, but it still needs to know its own boundaries (for later insert/remove operations):

Fragment DOM layout:

Virtual layer:
  Fragment VNode
  โ”œโ”€โ”€ Text VNode "item A"
  โ”œโ”€โ”€ Text VNode "item B"
  โ””โ”€โ”€ Text VNode "item C"

Real DOM layer:
  <!-- start anchor: stored at vnode.el -->
  "item A"
  "item B"
  "item C"
  <!-- end anchor: stored at vnode.anchor -->

On unmount: all DOM nodes between el and anchor are removed
// Core logic of processFragment (simplified)
function processFragment(n1, n2, container) {
  const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))
  const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))

  if (n1 == null) {
    // First mount: insert start anchor, mount children, insert end anchor
    hostInsert(fragmentStartAnchor, container)
    mountChildren(n2.children, container, fragmentEndAnchor)
    hostInsert(fragmentEndAnchor, container)
  } else {
    // Update: patch children
    patchChildren(n1, n2, container, fragmentEndAnchor)
  }
}

Element VNode vs Component VNode: Mounting Differences

Element VNode mount (mountElement):
  1. createElement(type)           โ†’ create real DOM node
  2. mountChildren(children)       โ†’ recursively process children
  3. patchProps(el, props)         โ†’ set class/style/events etc.
  4. insert(el, container)         โ†’ insert into DOM tree

Component VNode mount (mountComponent):
  1. createComponentInstance()     โ†’ create component instance object
  2. setupComponent(instance)      โ†’ execute setup(), process props/slots
  3. setupRenderEffect(instance)   โ†’ create reactive effect
     โ”œโ”€โ”€ First run: call render() to get subTree VNode
     โ”œโ”€โ”€ patch(null, subTree)      โ†’ recursively mount subTree
     โ””โ”€โ”€ Subsequent runs: re-execute when reactive data changes

Level 3 ยท Design Docs and Source Code (Senior Developers)

Why VNodes Use Plain Objects Instead of Classes

This is a critical design decision. Vue 2's VNode was defined with a class:

// Vue 2's approach
class VNode {
  constructor(tag, data, children, text, elm, ...) {
    this.tag = tag
    this.data = data
    // ... 20+ fields
  }
}

Vue 3 switched to factory functions returning plain objects:

// packages/runtime-core/src/vnode.ts
const vnode: VNode = {
  __v_isVNode: true,
  __v_skip: true,        // tells the reactivity system not to track this object
  type,
  key,
  ref,
  // ...
}

Three reasons:

  1. V8 Hidden Class (Inline Cache) optimization: Factory functions always return objects with the same field order, allowing V8 to classify them under the same hidden class, enabling Inline Cache hits. Class instances can theoretically do the same, but factory functions more reliably guarantee stable field order.
  2. The __v_skip: true marker: The reactivity system (reactive()) sees this marker and immediately skips the object, preventing VNodes from being turned into reactive objects and avoiding enormous amounts of unnecessary tracking.
  3. Tree-shaking friendly: Factory functions are easier to statically analyze and tree-shake than class methods.

Precise Semantics of patchFlag

// packages/shared/src/patchFlags.ts
export const enum PatchFlags {
  TEXT           = 1,        // dynamic text content
  CLASS          = 1 << 1,   // dynamic class binding
  STYLE          = 1 << 2,   // dynamic style binding
  PROPS          = 1 << 3,   // dynamic props (not class/style)
  FULL_PROPS     = 1 << 4,   // props with dynamic keys, needs full diff
  HYDRATE_EVENTS = 1 << 5,   // event listeners to process during SSR hydration
  STABLE_FRAGMENT = 1 << 6,  // Fragment whose children order never changes
  KEYED_FRAGMENT  = 1 << 7,  // Fragment with keys (v-for with key)
  UNKEYED_FRAGMENT = 1 << 8, // Fragment without keys (v-for without key)
  NEED_PATCH     = 1 << 9,   // needs patching (e.g. ref, directives)
  DYNAMIC_SLOTS  = 1 << 10,  // dynamic slots
  HOISTED        = -1,       // statically hoisted VNode, never updated
  BAIL           = -2,       // exit optimized mode, do full diff (after cloneVNode)
}

For <div :class="cls">{{ text }}</div>, the compiler generates:

createElementVNode(
  "div",
  { class: _ctx.cls },           // dynamic class
  toDisplayString(_ctx.text),    // dynamic text
  3  // patchFlag = CLASS(2) | TEXT(1) = 3
)

During updates, patchElement sees patchFlag = 3:

// packages/runtime-core/src/renderer.ts (simplified)
function patchElement(n1, n2) {
  const { patchFlag, dynamicProps } = n2

  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.CLASS) {
      // Only update class, don't touch style or other props
      hostPatchProp(el, 'class', null, n2.props!.class)
    }
    if (patchFlag & PatchFlags.TEXT) {
      // Only update text via direct textContent assignment
      if (n1.children !== n2.children) {
        hostSetElementText(el, n2.children as string)
      }
    }
    // All other checks are skipped
  } else if (patchFlag === PatchFlags.BAIL || patchFlag === 0) {
    // Full diff: compare all props
    patchProps(el, n2, oldProps, newProps)
  }
}

Memory Layout Analysis of a VNode Object

Memory estimate for a typical element VNode:

VNode memory layout (V8 64-bit environment):

Field                Type        Size (estimate)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
__v_isVNode         boolean     1 byte (flag bit in object header)
__v_skip            boolean     1 byte
type                pointer     8 bytes (points to string or object)
key                 pointer     8 bytes
ref                 pointer     8 bytes
props               pointer     8 bytes (points to props object)
children            pointer     8 bytes
component           null        8 bytes
el                  null        8 bytes
shapeFlag           int32       4 bytes  โ† the value of using a bitmask
patchFlag           int32       4 bytes
dynamicProps        null        8 bytes
dynamicChildren     null        8 bytes
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Object header + fields:   approximately 120โ€“160 bytes

If replaced with 10 independent boolean fields:
isElement/isComponent/... each boolean occupies 8 bytes (pointer slot) in a V8 object
Additional cost: 10 ร— 8 = 80 bytes per VNode

The shapeFlag Assignment Chain in Source Code

// packages/runtime-core/src/vnode.ts (key code path)

export function createBaseVNode(type, props, children, ...) {
  const vnode: VNode = {
    // ...
    shapeFlag: isString(type)
      ? ShapeFlags.ELEMENT
      : __FEATURE_SUSPENSE__ && isSuspense(type)
        ? ShapeFlags.SUSPENSE
        : isTeleport(type)
          ? ShapeFlags.TELEPORT
          : isObject(type)
            ? ShapeFlags.STATEFUL_COMPONENT
            : isFunction(type)
              ? ShapeFlags.FUNCTIONAL_COMPONENT
              : 0,
    // ...
  }

  // After normalizing children, update shapeFlag
  normalizeChildren(vnode, children)
  // Inside normalizeChildren:
  //   vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN   (if children is a string)
  //   vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN  (if children is an array)
  //   vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN  (if children is an object)

  return vnode
}

Why COMPONENT = STATEFUL | FUNCTIONAL = 6

This combined value lets the renderer match both component types with one bitwise operation rather than two separate checks:

// Suboptimal: two separate checks
if (
  (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) ||
  (shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT)
) { ... }

// Vue 3's actual approach: one check
if (shapeFlag & ShapeFlags.COMPONENT) {
  // ShapeFlags.COMPONENT = 6 = 0110
  // STATEFUL(4) = 0100 โ†’ 0100 & 0110 = 0100 โ‰  0 โ†’ true
  // FUNCTIONAL(2) = 0010 โ†’ 0010 & 0110 = 0010 โ‰  0 โ†’ true
  // ELEMENT(1) = 0001 โ†’ 0001 & 0110 = 0000 = 0 โ†’ false
}

This is the essence of bitmask design: carefully chosen bit assignments allow "group checks" to be performed in a single operation.


Level 4 ยท Edge Cases and Traps (Everyone)

Trap 1: Directly Mutating a VNode's Properties

// Dangerous: directly modifying a VNode causes unpredictable behavior
const vnode = h('div', { class: 'box' })
vnode.props.class = 'other'  // Never do this!

// Reasons:
// 1. The VNode may be statically hoisted (HOISTED), shared across all renders
// 2. Direct mutation bypasses the reactivity system; the next diff will produce wrong results
// 3. patchFlag already told the renderer which fields are static โ€” direct mutation breaks that contract

// Correct approach: use cloneVNode
const modified = cloneVNode(vnode, { class: 'other' })

Trap 2: Building a VNode Tree Inside an Async Function

// Problem: after await, the currentBlock context may point to the wrong Block
async function fetchAndRender() {
  const data = await fetch('/api/data')
  // โš ๏ธ After this await, the openBlock/createBlock context is lost
  return h('div', data.json())
}

// Safer approach: use reactive data in the synchronous setup() phase
const data = ref(null)
onMounted(async () => {
  data.value = await fetch('/api/data').then(r => r.json())
})
// Use v-if="data" in the template to handle the async state

Trap 3: Misunderstanding the Scope of key

<template>
  <!-- Two different v-for loops with overlapping key values don't interfere -->
  <!-- key only works among sibling nodes under the same parent -->
  <div v-for="item in list1" :key="item.id">{{ item.name }}</div>
  <div v-for="item in list2" :key="item.id">{{ item.name }}</div>
  <!-- These two v-for loops' keys are completely independent -->

  <!-- Wrong: adding key to a parent element won't reset child components -->
  <div key="parent-key">
    <MyComponent />  <!-- key is on the div, MyComponent is NOT reset -->
  </div>

  <!-- Correct: key must be placed directly on the component to reset it -->
  <MyComponent :key="someValue" />
</template>

Trap 4: Statically Hoisted VNodes Corrupted by cloneVNode

// The compiler hoists static nodes to module level
// _hoisted_1 is created only once at module initialization
const _hoisted_1 = createElementVNode("div", { class: "static-box" }, "Never changes")
// _hoisted_1.patchFlag === PatchFlags.HOISTED === -1

// If you use cloneVNode to modify it:
const cloned = cloneVNode(_hoisted_1, { 'data-extra': 'value' })
// cloned.patchFlag becomes PatchFlags.BAIL (-2)
// This means the cloned version will do a full diff on every update
// Worse performance than simply declaring the attribute as dynamic in the template

Trap 5: shapeFlag Misjudgment for Functional Components

// The way you define a component determines its shapeFlag
const Comp1 = { render() { return h('div') } }            // shapeFlag: STATEFUL_COMPONENT(4)
const Comp2 = (props) => h('div')                         // shapeFlag: FUNCTIONAL_COMPONENT(2)
const Comp3 = defineComponent({ render() { return h('div') } })  // shapeFlag: STATEFUL_COMPONENT(4)

// defineComponent always returns the passed-in object (isObject โ†’ STATEFUL)
// Even if you pass in a function:
const Comp4 = defineComponent((props) => () => h('div'))
// setup returns a render function, but Comp4 itself is an object wrapped by defineComponent
// shapeFlag is still STATEFUL_COMPONENT(4), not FUNCTIONAL_COMPONENT(2)

Trap 6: Misinterpreting null dynamicChildren After cloneVNode

// cloneVNode clears dynamicChildren
const original = createElementVNode('div', null, 'text', PatchFlags.TEXT)
// original.dynamicChildren = [...] (collected by the current Block)

const cloned = cloneVNode(original)
// cloned.dynamicChildren = null  โ† cleared!
// cloned.patchFlag = original.patchFlag | PatchFlags.BAIL = -2

// Problem: if you place cloned inside an openBlock/createBlock context
// its dynamicChildren is null, so Block optimization doesn't apply
// The renderer falls back to full diff mode

Chapter Summary

  1. VNode is a single-frame blueprint: On average 11 fields. type determines the rendering path, shapeFlag encodes type information using a bitmask, and patchFlag tells the renderer which parts are dynamic.

  2. The value of ShapeFlag bitmasks: A single int32 field replaces 10 booleans, saving approximately 80 bytes of object footprint and turning type checking from property accesses into single bitwise operations (2โ€“5x faster on hot paths).

  3. The three creation functions have distinct roles: h() is the user-facing layer (most normalization), createVNode() is the internal layer, and createElementVNode() is the compiler's fast path (skips type checks). Understanding this hierarchy helps optimize hand-written render functions.

  4. Fragment's DOM anchor mechanism: A Fragment VNode marks its boundaries in the DOM through two empty text nodes โ€” el (start anchor) and anchor (end anchor). This allows Fragments to be removed and moved without knowing their parent node.

  5. Never directly mutate a VNode: VNodes are immutable blueprints. Statically hoisted VNodes are shared across all render cycles; direct mutation breaks patchFlag contracts and causes the renderer to behave incorrectly. Use cloneVNode() when you need modifications.

Rate this chapter
4.6  / 5  (21 ratings)

๐Ÿ’ฌ Comments