]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
perf: advance to parent nextSibling only when insertion is the last in parent
authordaiwei <daiwei521@126.com>
Thu, 16 Oct 2025 08:07:00 +0000 (16:07 +0800)
committerdaiwei <daiwei521@126.com>
Thu, 16 Oct 2025 08:55:48 +0000 (16:55 +0800)
17 files changed:
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts
packages/compiler-vapor/src/generators/operation.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/transformChildren.ts
packages/runtime-vapor/src/apiCreateDynamicComponent.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/apiCreateIf.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/insertionState.ts

index d2cd54c9e1c94b0eeb0ffff7f3e65d77302964dd..0120f5f487827e4f3cbfdbe53d7cb94a24381b37 100644 (file)
@@ -38,7 +38,7 @@ export function render(_ctx) {
     "default": () => {
       const n0 = _createIf(() => (true), () => {
         const n3 = t0()
-        _setInsertionState(n3, null)
+        _setInsertionState(n3, null, true)
         const n2 = _createComponentWithFallback(_component_Bar)
         _withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
         return n3
@@ -158,7 +158,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
   const n0 = t0()
   const n3 = t1()
   const n2 = _child(n3, 1)
-  _setInsertionState(n3, 0)
+  _setInsertionState(n3, 0, true)
   const n1 = _createComponentWithFallback(_component_Comp)
   _renderEffect(() => {
     _setProp(n3, "id", _ctx.foo)
@@ -226,7 +226,7 @@ export function render(_ctx) {
   const n4 = _child(p0, 0)
   _setInsertionState(n6, n5)
   const n0 = _createComponentWithFallback(_component_Comp)
-  _setInsertionState(n6, n7)
+  _setInsertionState(n6, n7, true)
   const n1 = _createIf(() => (true), () => {
     const n3 = t0()
     return n3
@@ -244,9 +244,9 @@ export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
   const n3 = t0()
   const n1 = _child(n3, 0)
-  _setInsertionState(n1, null)
+  _setInsertionState(n1, null, true)
   const n0 = _createSlot("default", null)
-  _setInsertionState(n3, 1)
+  _setInsertionState(n3, 1, true)
   const n2 = _createComponentWithFallback(_component_Comp)
   return n3
 }"
index 8a83143b9b720b37d326245d05c49b6779faab40..3e6edc8ae9db58918e992bbba9f04b863cf57aba 100644 (file)
@@ -51,7 +51,7 @@ export function render(_ctx) {
         return n5
       }, () => {
         const n14 = t2()
-        _setInsertionState(n14, 0)
+        _setInsertionState(n14, 0, true)
         const n9 = _createIf(() => (_ctx.c), () => {
           const n11 = t1()
           return n11
index 99f64c8cf23e31ccc8883cc9a9d181ecc5475175..c75f67c36df82dcfd1e876164fa67b4c511e0a1d 100644 (file)
@@ -8,7 +8,7 @@ const t1 = _template("<div><div></div><!><div></div></div>", true)
 export function render(_ctx) {
   const n4 = t1()
   const n3 = _next(_child(n4), 1)
-  _setInsertionState(n4, n3)
+  _setInsertionState(n4, n3, true)
   const n0 = _createIf(() => (1), () => {
     const n2 = t0()
     return n2
index cccc9200f880c9c19e712775f657a431b569e4d8..6b75644630ab50a826ce1710dca47bfdeaf40a97 100644 (file)
@@ -87,7 +87,7 @@ const t1 = _template("<div></div>", true)
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
     const n5 = t1()
-    _setInsertionState(n5, null)
+    _setInsertionState(n5, null, true)
     const n2 = _createFor(() => (_for_item0.value), (_for_item1) => {
       const n4 = t0()
       const x4 = _txt(n4)
index c60ae16be0502c6f8072c040efc754a8c2ea98c8..a8dc5aa459006a79826e40d3f3895537bfd3ff46 100644 (file)
@@ -149,7 +149,7 @@ export function render(_ctx) {
     const n2 = t0()
     return n2
   })
-  _setInsertionState(n8, null)
+  _setInsertionState(n8, null, true)
   const n3 = _createIf(() => (_ctx.bar), () => {
     const n5 = t1()
     return n5
index 2fcd18da1df452725cc6016c3aa63911b6d276ef..ae5e9df743a66761aea71b10341f9d223b6b01f1 100644 (file)
@@ -42,7 +42,7 @@ const t0 = _template("<div></div>", true)
 export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
   const n1 = t0()
-  _setInsertionState(n1, null)
+  _setInsertionState(n1, null, true)
   const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true)
   return n1
 }"
index 9b11f074c40d009751f819b13be603e3b2df7ebc..3b804cb6eccfd0b33e4978bad3154f1f32d5d471 100644 (file)
@@ -354,7 +354,7 @@ export function render(_ctx) {
   const n6 = t0()
   _setInsertionState(n6, null)
   const n0 = _createSlot("foo", null)
-  _setInsertionState(n6, null)
+  _setInsertionState(n6, null, true)
   const n1 = _createIf(() => (true), () => {
     const n3 = _createComponentWithFallback(_component_Foo)
     return n3
index d41ed2ec476e07c9f520eb8bb3ef00f9fd10fc4d..9d5226921a4cf2de2800693b0211a74b76e3bfe8 100644 (file)
@@ -70,7 +70,7 @@ describe('compiler: children transform', () => {
     )
     // ensure the insertion anchor is generated before the insertion statement
     expect(code).toMatch(`const n3 = _next(_child(n4), 1)`)
-    expect(code).toMatch(`_setInsertionState(n4, n3)`)
+    expect(code).toMatch(`_setInsertionState(n4, n3, true)`)
     expect(code).toMatchSnapshot()
   })
 })
index 28cf7a0b9d8b45df57ad569c13a53dea3abe3ce9..804505160248d5bc074f51d4f77ee410bb13048f 100644 (file)
@@ -168,7 +168,7 @@ function genInsertionState(
   operation: InsertionStateTypes,
   context: CodegenContext,
 ): CodeFragment[] {
-  const { parent, anchor, append } = operation
+  const { parent, anchor, append, last } = operation
   return [
     NEWLINE,
     ...genCall(
@@ -185,6 +185,7 @@ function genInsertionState(
               ? 'null'
               : `${anchor}`
             : `n${anchor}`,
+      last && 'true',
     ),
   ]
 }
index 5bea27fc04338148f8b9ddea2b43153fd40d2163..492dae6e7b30f9d5b92d6348533a500e2441d7e2 100644 (file)
@@ -81,6 +81,7 @@ export interface IfIRNode extends BaseIRNode {
   parent?: number
   anchor?: number
   append?: boolean
+  last?: boolean
 }
 
 export interface IRFor {
@@ -101,6 +102,7 @@ export interface ForIRNode extends BaseIRNode, IRFor {
   parent?: number
   anchor?: number
   append?: boolean
+  last?: boolean
 }
 
 export interface SetPropIRNode extends BaseIRNode {
@@ -203,6 +205,8 @@ export interface CreateComponentIRNode extends BaseIRNode {
   parent?: number
   anchor?: number
   append?: boolean
+  last?: boolean
+
   scopeId?: string | null
 }
 
@@ -221,6 +225,7 @@ export interface SlotOutletIRNode extends BaseIRNode {
   parent?: number
   anchor?: number
   append?: boolean
+  last?: boolean
 }
 
 export interface GetTextChildIRNode extends BaseIRNode {
index 973b332b5b856b1e83ef7d8a2e459d8512bbc74a..cdff90edeb2b77d998679434260f723522e8456f 100644 (file)
@@ -8,6 +8,7 @@ import {
   DynamicFlag,
   type IRDynamicInfo,
   IRNodeTypes,
+  type InsertionStateTypes,
   isBlockOperation,
 } from '../ir'
 
@@ -61,11 +62,12 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
   let prevDynamics: IRDynamicInfo[] = []
   let staticCount = 0
   let dynamicCount = 0
+  let lastInsertionChild: IRDynamicInfo | undefined
   const children = context.dynamic.children
 
   for (const [index, child] of children.entries()) {
     if (child.flags & DynamicFlag.INSERT) {
-      prevDynamics.push(child)
+      prevDynamics.push((lastInsertionChild = child))
     }
 
     if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
@@ -86,7 +88,17 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
   }
 
   if (prevDynamics.length) {
-    registerInsertion(prevDynamics, context, dynamicCount + staticCount, true)
+    registerInsertion(
+      prevDynamics,
+      context,
+      // the logical index of append child
+      dynamicCount + staticCount,
+      true,
+    )
+  }
+
+  if (lastInsertionChild && lastInsertionChild.operation) {
+    ;(lastInsertionChild.operation! as InsertionStateTypes).last = true
   }
 }
 
index cc7bd6aaf2fc778eb58bbb95752058efeaae17c5..6e051a4e25d5b22ee0f49e8e338cfd48e3fcafd7 100644 (file)
@@ -7,6 +7,7 @@ import type { RawSlots } from './componentSlots'
 import {
   insertionAnchor,
   insertionParent,
+  isLastInsertion,
   resetInsertionState,
 } from './insertionState'
 import { advanceHydrationNode, isHydrating } from './dom/hydration'
@@ -22,6 +23,7 @@ export function createDynamicComponent(
 ): VaporFragment {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
+  const _isLastInsertion = isLastInsertion
   if (!isHydrating) resetInsertionState()
 
   const frag =
@@ -51,7 +53,7 @@ export function createDynamicComponent(
   if (!isHydrating) {
     if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
   } else {
-    if (_insertionAnchor !== undefined) {
+    if (_isLastInsertion) {
       advanceHydrationNode(_insertionParent!)
     }
   }
index 38c60b99168e3541f9393061c8d953b019fdcab1..ae70d6c31e7149f74ded37f0a82247e3c4d7bb3a 100644 (file)
@@ -41,6 +41,7 @@ import { ForFragment, VaporFragment, findBlockNode } from './fragment'
 import {
   insertionAnchor,
   insertionParent,
+  isLastInsertion,
   resetInsertionState,
 } from './insertionState'
 import { applyTransitionHooks } from './components/Transition'
@@ -94,6 +95,7 @@ export const createFor = (
 ): ForFragment => {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
+  const _isLastInsertion = isLastInsertion
   if (isHydrating) {
     locateHydrationNode()
   } else {
@@ -485,9 +487,7 @@ export const createFor = (
   if (!isHydrating) {
     if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
   } else {
-    advanceHydrationNode(
-      _insertionAnchor !== undefined ? _insertionParent! : parentAnchor!,
-    )
+    advanceHydrationNode(_isLastInsertion ? _insertionParent! : parentAnchor!)
   }
 
   return frag
index 2183cb9677f3a78acdf90a9210cb0ddd10b074c6..b9764f7d1a53c87699378a15998ff3128d16de94 100644 (file)
@@ -3,6 +3,7 @@ import { advanceHydrationNode, isHydrating } from './dom/hydration'
 import {
   insertionAnchor,
   insertionParent,
+  isLastInsertion,
   resetInsertionState,
 } from './insertionState'
 import { renderEffect } from './renderEffect'
@@ -16,6 +17,7 @@ export function createIf(
 ): Block {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
+  const _isLastInsertion = isLastInsertion
   if (!isHydrating) resetInsertionState()
 
   let frag: Block
@@ -30,13 +32,7 @@ export function createIf(
   if (!isHydrating) {
     if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
   } else {
-    // After block node hydration is completed, advance currentHydrationNode to
-    // _insertionParent's next sibling if _insertionAnchor has a value
-    // _insertionAnchor values:
-    // 1. Node type: _insertionAnchor is a static node, no hydration needed
-    // 2. null: block node is appended, potentially without next sibling
-    // 3. 0: next sibling of current block node is static, no hydration needed
-    if (_insertionAnchor !== undefined) {
+    if (_isLastInsertion) {
       advanceHydrationNode(_insertionParent!)
     }
   }
index 0139d9cf1c63f0dea56d5cfdfcf641af7a8c65df..63d363bb77ebc021c8512bcb4568bed369af06d3 100644 (file)
@@ -81,6 +81,7 @@ import { type TeleportFragment, isVaporTeleport } from './components/Teleport'
 import {
   insertionAnchor,
   insertionParent,
+  isLastInsertion,
   resetInsertionState,
 } from './insertionState'
 import { DynamicFragment } from './fragment'
@@ -167,6 +168,7 @@ export function createComponent(
 ): VaporComponentInstance {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
+  const _isLastInsertion = isLastInsertion
   if (isHydrating) {
     locateHydrationNode()
   } else {
@@ -204,7 +206,7 @@ export function createComponent(
       if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
     } else {
       frag.hydrate()
-      if (_insertionAnchor !== undefined) {
+      if (_isLastInsertion) {
         advanceHydrationNode(_insertionParent!)
       }
     }
@@ -219,7 +221,7 @@ export function createComponent(
       if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
     } else {
       frag.hydrate()
-      if (_insertionAnchor !== undefined) {
+      if (_isLastInsertion) {
         advanceHydrationNode(_insertionParent!)
       }
     }
@@ -610,6 +612,7 @@ export function createComponentWithFallback(
 
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
+  const _isLastInsertion = isLastInsertion
   if (isHydrating) {
     locateHydrationNode()
   } else {
@@ -650,7 +653,7 @@ export function createComponentWithFallback(
   if (!isHydrating) {
     if (_insertionParent) insert(el, _insertionParent, _insertionAnchor)
   } else {
-    if (_insertionAnchor !== undefined) {
+    if (_isLastInsertion) {
       advanceHydrationNode(_insertionParent!)
     }
   }
index f01034426bd9fafbaae551b9f5af712d1c58fd50..ecbd81a0f10b6d4457aa4915b4c7826cede95200 100644 (file)
@@ -7,6 +7,7 @@ import { renderEffect } from './renderEffect'
 import {
   insertionAnchor,
   insertionParent,
+  isLastInsertion,
   resetInsertionState,
 } from './insertionState'
 import {
@@ -115,6 +116,7 @@ export function createSlot(
 ): Block {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
+  const _isLastInsertion = isLastInsertion
   if (!isHydrating) resetInsertionState()
 
   const instance = i || (currentInstance as VaporComponentInstance)
@@ -174,7 +176,7 @@ export function createSlot(
     if (_insertionParent) {
       updateLastLogicalChild(_insertionParent!, fragment.anchor)
     }
-    if (_insertionAnchor !== undefined) {
+    if (_isLastInsertion) {
       advanceHydrationNode(_insertionParent!)
     }
   }
index a4bd23ed4cab64e6c5d10c072cde80c6f6150850..993dc65b0d2f380579f100914598210b2a27ac68 100644 (file)
@@ -22,6 +22,11 @@ export type InsertionParent = ParentNode & {
 export let insertionParent: InsertionParent | undefined
 export let insertionAnchor: Node | 0 | undefined | null
 
+// indicates whether the insertion is the last one in the parent.
+// if true, means no more nodes need to be hydrated after this insertion,
+// advancing current hydration node to parent nextSibling
+export let isLastInsertion: boolean | undefined
+
 /**
  * This function is called before a block type that requires insertion
  * (component, slot outlet, if, for) is created. The state is used for actual
@@ -30,8 +35,10 @@ export let insertionAnchor: Node | 0 | undefined | null
 export function setInsertionState(
   parent: ParentNode & { $fc?: Node | null },
   anchor?: Node | 0 | null | number,
+  last?: boolean,
 ): void {
   insertionParent = parent
+  isLastInsertion = last
 
   if (anchor !== undefined) {
     if (isHydrating) {
@@ -51,5 +58,5 @@ export function setInsertionState(
 }
 
 export function resetInsertionState(): void {
-  insertionParent = insertionAnchor = undefined
+  insertionParent = insertionAnchor = isLastInsertion = undefined
 }