فهرست منبع

perf(vapor): reduce template ref codegen size (#14868)

edison 3 هفته پیش
والد
کامیت
6bb7d3d6a1

+ 2 - 3
packages/compiler-vapor/__tests__/__snapshots__/staticTemplate.spec.ts.snap

@@ -91,13 +91,12 @@ export function render(_ctx) {
 `;
 
 exports[`static template marker > does not mark single-root element with ref 1`] = `
-"import { createTemplateRefSetter as _createTemplateRefSetter, template as _template } from 'vue';
+"import { setStaticTemplateRef as _setStaticTemplateRef, template as _template } from 'vue';
 const t0 = _template("<div>", 1)
 
 export function render(_ctx) {
-  const _setTemplateRef = _createTemplateRefSetter()
   const n0 = t0()
-  _setTemplateRef(n0, "el")
+  _setStaticTemplateRef(n0, "el")
   return n0
 }"
 `;

+ 166 - 8
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap

@@ -1,17 +1,106 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`compiler: template ref transform > dynamic ref 1`] = `
+exports[`compiler: template ref transform > component static ref 1`] = `
+"import { createAssetComponent as _createAssetComponent, setStaticTemplateRef as _setStaticTemplateRef } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createAssetComponent("Foo", null, null, true)
+  _setStaticTemplateRef(n0, "foo")
+  return n0
+}"
+`;
+
+exports[`compiler: template ref transform > dynamic and function refs 1`] = `
 "import { createTemplateRefSetter as _createTemplateRefSetter, renderEffect as _renderEffect, template as _template } from 'vue';
-const t0 = _template("<div>", 1)
+const t0 = _template("<div>")
 
 export function render(_ctx) {
   const _setTemplateRef = _createTemplateRefSetter()
   const n0 = t0()
-  _renderEffect(() => _setTemplateRef(n0, _ctx.foo))
+  const n1 = t0()
+  _renderEffect(() => {
+    const _foo = _ctx.foo
+    _setTemplateRef(n0, _foo)
+    _setTemplateRef(n1, bar => { _foo.value = bar })
+  })
+  return [n0, n1]
+}"
+`;
+
+exports[`compiler: template ref transform > dynamic and static refs 1`] = `
+"import { setStaticTemplateRef as _setStaticTemplateRef, setTemplateRefBinding as _setTemplateRefBinding, template as _template } from 'vue';
+const t0 = _template("<div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  _setStaticTemplateRef(n1, "foo")
+  _setTemplateRefBinding(n0, () => _ctx.bar)
+  return [n0, n1]
+}"
+`;
+
+exports[`compiler: template ref transform > dynamic component static ref 1`] = `
+"import { createDynamicComponent as _createDynamicComponent, setStaticTemplateRef as _setStaticTemplateRef } from 'vue';
+
+export function render(_ctx) {
+  const n0 = _createDynamicComponent(() => (_ctx.view), null, null, true)
+  _setStaticTemplateRef(n0, "foo")
   return n0
 }"
 `;
 
+exports[`compiler: template ref transform > dynamic ref (inline mode) 1`] = `
+"
+  const n0 = t0()
+  _setTemplateRefBinding(n0, () => foo, undefined, undefined, "foo")
+  return n0
+"
+`;
+
+exports[`compiler: template ref transform > dynamic ref + v-for 1`] = `
+"import { createTemplateRefSetter as _createTemplateRefSetter, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div>")
+
+export function render(_ctx) {
+  const _setTemplateRef = _createTemplateRefSetter()
+  const n0 = _createFor(() => ([1,2,3]), (_for_item0) => {
+    const n2 = t0()
+    _renderEffect(() => _setTemplateRef(n2, _ctx.foo, true))
+    return n2
+  }, undefined, 12)
+  return n0
+}"
+`;
+
+exports[`compiler: template ref transform > dynamic ref 1`] = `
+"import { setTemplateRefBinding as _setTemplateRefBinding, template as _template } from 'vue';
+const t0 = _template("<div>", 1)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setTemplateRefBinding(n0, () => _ctx.foo)
+  return n0
+}"
+`;
+
+exports[`compiler: template ref transform > dynamic ref in slot uses owner setter 1`] = `
+"import { createTemplateRefSetter as _createTemplateRefSetter, setTemplateRefBinding as _setTemplateRefBinding, createAssetComponent as _createAssetComponent, template as _template } from 'vue';
+const t0 = _template("<div>")
+
+export function render(_ctx) {
+  const _setTemplateRef = _createTemplateRefSetter()
+  const n1 = _createAssetComponent("Comp", null, {
+    "default": () => {
+      const n0 = t0()
+      _setTemplateRefBinding(n0, () => _ctx.refName, _setTemplateRef)
+      return n0
+    }
+  }, true)
+  return n1
+}"
+`;
+
 exports[`compiler: template ref transform > function ref 1`] = `
 "import { createTemplateRefSetter as _createTemplateRefSetter, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div>", 1)
@@ -31,6 +120,36 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: template ref transform > multiple dynamic refs 1`] = `
+"import { createTemplateRefSetter as _createTemplateRefSetter, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div>")
+
+export function render(_ctx) {
+  const _setTemplateRef = _createTemplateRefSetter()
+  const n0 = t0()
+  const n1 = t0()
+  _renderEffect(() => {
+    _setTemplateRef(n0, _ctx.foo)
+    _setTemplateRef(n1, _ctx.bar)
+  })
+  return [n0, n1]
+}"
+`;
+
+exports[`compiler: template ref transform > multiple static refs 1`] = `
+"import { createTemplateRefSetter as _createTemplateRefSetter, template as _template } from 'vue';
+const t0 = _template("<div>")
+
+export function render(_ctx) {
+  const _setTemplateRef = _createTemplateRefSetter()
+  const n0 = t0()
+  const n1 = t0()
+  _setTemplateRef(n0, "foo")
+  _setTemplateRef(n1, "bar")
+  return [n0, n1]
+}"
+`;
+
 exports[`compiler: template ref transform > ref + v-for 1`] = `
 "import { createTemplateRefSetter as _createTemplateRefSetter, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<div>")
@@ -61,23 +180,62 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: template ref transform > simple function ref 1`] = `
+"import { setTemplateRefBinding as _setTemplateRefBinding, template as _template } from 'vue';
+const t0 = _template("<div>", 1)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setTemplateRefBinding(n0, () => bar => { _ctx.foo.value = bar })
+  return n0
+}"
+`;
+
+exports[`compiler: template ref transform > static and dynamic refs 1`] = `
+"import { setStaticTemplateRef as _setStaticTemplateRef, setTemplateRefBinding as _setTemplateRefBinding, template as _template } from 'vue';
+const t0 = _template("<div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  _setStaticTemplateRef(n0, "foo")
+  _setTemplateRefBinding(n1, () => _ctx.bar)
+  return [n0, n1]
+}"
+`;
+
 exports[`compiler: template ref transform > static ref (inline mode) 1`] = `
 "
-  const _setTemplateRef = _createTemplateRefSetter()
   const n0 = t0()
-  _setTemplateRef(n0, foo, null, "foo")
+  _setStaticTemplateRef(n0, foo, null, "foo")
   return n0
 "
 `;
 
 exports[`compiler: template ref transform > static ref 1`] = `
-"import { createTemplateRefSetter as _createTemplateRefSetter, template as _template } from 'vue';
+"import { setStaticTemplateRef as _setStaticTemplateRef, template as _template } from 'vue';
 const t0 = _template("<div>", 1)
 
 export function render(_ctx) {
-  const _setTemplateRef = _createTemplateRefSetter()
   const n0 = t0()
-  _setTemplateRef(n0, "foo")
+  _setStaticTemplateRef(n0, "foo")
   return n0
 }"
 `;
+
+exports[`compiler: template ref transform > static ref in slot uses owner setter 1`] = `
+"import { createTemplateRefSetter as _createTemplateRefSetter, createAssetComponent as _createAssetComponent, template as _template } from 'vue';
+const t0 = _template("<div>")
+
+export function render(_ctx) {
+  const _setTemplateRef = _createTemplateRefSetter()
+  const n1 = _createAssetComponent("Comp", null, {
+    "default": () => {
+      const n0 = t0()
+      _setTemplateRef(n0, "foo")
+      return n0
+    }
+  }, true)
+  return n1
+}"
+`;

+ 142 - 3
packages/compiler-vapor/__tests__/transforms/transformTemplateRef.spec.ts

@@ -9,6 +9,7 @@ import {
   transformTemplateRef,
   transformVFor,
   transformVIf,
+  transformVSlot,
 } from '../../src'
 import { makeCompile } from './_utils'
 
@@ -18,6 +19,7 @@ const compileWithTransformRef = makeCompile({
     transformVFor,
     transformTemplateRef,
     transformElement,
+    transformVSlot,
     transformChildren,
   ],
 })
@@ -45,8 +47,8 @@ describe('compiler: template ref transform', () => {
       },
     })
     expect(code).matchSnapshot()
-    expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
-    expect(code).contains('_setTemplateRef(n0, "foo")')
+    expect(code).contains('_setStaticTemplateRef(n0, "foo")')
+    expect(code).not.contains('_createTemplateRefSetter')
   })
 
   test('static ref (inline mode)', () => {
@@ -56,7 +58,60 @@ describe('compiler: template ref transform', () => {
     })
     expect(code).matchSnapshot()
     // pass the actual ref and ref key
-    expect(code).contains('_setTemplateRef(n0, foo, null, "foo")')
+    expect(code).contains('_setStaticTemplateRef(n0, foo, null, "foo")')
+    expect(code).not.contains('_createTemplateRefSetter')
+  })
+
+  test('multiple static refs', () => {
+    const { code } = compileWithTransformRef(
+      `<div ref="foo" /><div ref="bar" />`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
+    expect(code).contains('_setTemplateRef(n0, "foo")')
+    expect(code).contains('_setTemplateRef(n1, "bar")')
+    expect(code).not.contains('_setStaticTemplateRef')
+    expect(code).not.contains('_setTemplateRefBinding')
+  })
+
+  test('static and dynamic refs', () => {
+    const { code } = compileWithTransformRef(
+      `<div ref="foo" /><div :ref="bar" />`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).not.contains('_createTemplateRefSetter')
+    expect(code).contains('_setStaticTemplateRef(n0, "foo")')
+    expect(code).contains('_setTemplateRefBinding(n1, () => _ctx.bar)')
+    expect(code).not.contains('_setTemplateRefBinding(n1, () => _ctx.bar,')
+  })
+
+  test('dynamic and static refs', () => {
+    const { code } = compileWithTransformRef(
+      `<div :ref="bar" /><div ref="foo" />`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).not.contains('_createTemplateRefSetter')
+    expect(code).contains('_setTemplateRefBinding(n0, () => _ctx.bar)')
+    expect(code).not.contains('_setTemplateRefBinding(n0, () => _ctx.bar,')
+    expect(code).contains('_setStaticTemplateRef(n1, "foo")')
+  })
+
+  test('component static ref', () => {
+    const { code } = compileWithTransformRef(`<Foo ref="foo" />`)
+    expect(code).matchSnapshot()
+    expect(code).contains('_setStaticTemplateRef(n0, "foo")')
+    expect(code).not.contains('_createTemplateRefSetter')
+    expect(code).not.contains('_setTemplateRefBinding')
+  })
+
+  test('dynamic component static ref', () => {
+    const { code } = compileWithTransformRef(
+      `<component :is="view" ref="foo" />`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).contains('_setStaticTemplateRef(n0, "foo")')
+    expect(code).not.contains('_createTemplateRefSetter')
+    expect(code).not.contains('_setTemplateRefBinding')
   })
 
   test('dynamic ref', () => {
@@ -82,8 +137,79 @@ describe('compiler: template ref transform', () => {
       },
     ])
     expect(code).matchSnapshot()
+    expect(code).contains('_setTemplateRefBinding(n0, () => _ctx.foo)')
+    expect(code).not.contains('_createTemplateRefSetter')
+    expect(code).not.contains('_renderEffect')
+  })
+
+  test('dynamic ref (inline mode)', () => {
+    const { code } = compileWithTransformRef(`<div :ref="foo" />`, {
+      inline: true,
+      bindingMetadata: { foo: BindingTypes.SETUP_REF },
+    })
+    expect(code).matchSnapshot()
+    expect(code).contains(
+      '_setTemplateRefBinding(n0, () => foo, undefined, undefined, "foo")',
+    )
+    expect(code).not.contains('_createTemplateRefSetter')
+  })
+
+  test('multiple dynamic refs', () => {
+    const { code } = compileWithTransformRef(
+      `<div :ref="foo" /><div :ref="bar" />`,
+    )
+    expect(code).matchSnapshot()
     expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
+    expect(code).contains('_renderEffect(() => {')
     expect(code).contains('_setTemplateRef(n0, _ctx.foo)')
+    expect(code).contains('_setTemplateRef(n1, _ctx.bar)')
+    expect(code).not.contains('_setTemplateRefBinding')
+  })
+
+  test('dynamic and function refs', () => {
+    const { code } = compileWithTransformRef(
+      `<div :ref="foo" /><div :ref="bar => { foo.value = bar }" />`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
+    expect(code).contains('_renderEffect(() => {')
+    expect(code).contains('_setTemplateRef(n0, _foo)')
+    expect(code).contains('_setTemplateRef(n1, bar => { _foo.value = bar })')
+    expect(code).not.contains('_setTemplateRefBinding')
+  })
+
+  test('dynamic ref in slot uses owner setter', () => {
+    const { code } = compileWithTransformRef(
+      `<Comp><div :ref="refName" /></Comp>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
+    expect(code).contains(
+      '_setTemplateRefBinding(n0, () => _ctx.refName, _setTemplateRef)',
+    )
+  })
+
+  test('static ref in slot uses owner setter', () => {
+    const { code } = compileWithTransformRef(`<Comp><div ref="foo" /></Comp>`)
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
+    expect(code).contains('_setTemplateRef(n0, "foo")')
+    expect(code).not.contains('_setStaticTemplateRef')
+  })
+
+  test('simple function ref', () => {
+    const { code } = compileWithTransformRef(
+      `<div :ref="bar => { foo.value = bar }" />`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains(
+      '_setTemplateRefBinding(n0, () => bar => { _ctx.foo.value = bar })',
+    )
+    expect(code).not.contains('_createTemplateRefSetter')
+    expect(code).not.contains('_renderEffect')
   })
 
   test('function ref', () => {
@@ -168,4 +294,17 @@ describe('compiler: template ref transform', () => {
     expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
     expect(code).contains('_setTemplateRef(n2, "foo", true)')
   })
+
+  test('dynamic ref + v-for', () => {
+    const { code } = compileWithTransformRef(
+      `<div :ref="foo" v-for="item in [1,2,3]" />`,
+    )
+
+    expect(code).matchSnapshot()
+    expect(code).contains('const _setTemplateRef = _createTemplateRefSetter()')
+    expect(code).contains(
+      '_renderEffect(() => _setTemplateRef(n2, _ctx.foo, true))',
+    )
+    expect(code).not.contains('_setTemplateRefBinding')
+  })
 })

+ 48 - 7
packages/compiler-vapor/src/generate.ts

@@ -3,7 +3,14 @@ import type {
   BaseCodegenResult,
   SimpleExpressionNode,
 } from '@vue/compiler-dom'
-import type { BlockIRNode, CoreHelper, RootIRNode, VaporHelper } from './ir'
+import type {
+  BlockIRNode,
+  CoreHelper,
+  RootIRNode,
+  SetTemplateRefIRNode,
+  VaporHelper,
+} from './ir'
+import { IRNodeTypes } from './ir'
 import { extend, remove } from '@vue/shared'
 import { genBlockContent } from './generators/block'
 import { genTemplates } from './generators/template'
@@ -35,6 +42,10 @@ export class CodegenContext {
 
   helpers: Map<string, string> = new Map()
 
+  needsTemplateRefSetter: boolean = false
+  staticTemplateRefHelperCandidate?: SetTemplateRefIRNode
+  inSlotBlock: boolean = false
+
   helper = (name: CoreHelper | VaporHelper): string => {
     if (this.helpers.has(name)) {
       return this.helpers.get(name)!
@@ -116,6 +127,12 @@ export class CodegenContext {
     return (): BlockIRNode => (this.block = parent)
   }
 
+  enterSlotBlock() {
+    const parent = this.inSlotBlock
+    this.inSlotBlock = true
+    return (): boolean => (this.inSlotBlock = parent)
+  }
+
   scopeLevel: number = 0
   enterScope(): [level: number, exit: () => number] {
     return [this.scopeLevel++, () => this.scopeLevel--] as const
@@ -206,6 +223,9 @@ export class CodegenContext {
         : [],
     )
     this.initNextIdMap()
+    this.staticTemplateRefHelperCandidate = getStaticTemplateRefHelperCandidate(
+      ir.block,
+    )
   }
 }
 
@@ -238,13 +258,18 @@ export function generate(
   }
 
   push(INDENT_START)
-  if (ir.hasTemplateRef) {
-    push(
-      NEWLINE,
-      `const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`,
-    )
+  // Pre-register to keep fallback template-ref helper ordering stable; remove it
+  // below when all refs lower to binding helpers.
+  const templateRefSetterHelper = ir.hasTemplateRef
+    ? context.helper('createTemplateRefSetter')
+    : undefined
+  const body = genBlockContent(ir.block, context, true)
+  if (context.needsTemplateRefSetter) {
+    push(NEWLINE, `const ${setTemplateRefIdent} = ${templateRefSetterHelper}()`)
+  } else if (templateRefSetterHelper) {
+    context.helpers.delete('createTemplateRefSetter')
   }
-  push(...genBlockContent(ir.block, context, true))
+  push(...body)
   push(INDENT_END, NEWLINE)
 
   if (!inline) {
@@ -304,3 +329,19 @@ function genAssetImports({ ir }: CodegenContext) {
   }
   return imports
 }
+
+function getStaticTemplateRefHelperCandidate(
+  block: BlockIRNode,
+): SetTemplateRefIRNode | undefined {
+  if (block.operation.length !== 1) return
+
+  const operation = block.operation[0]
+  if (
+    operation.type === IRNodeTypes.SET_TEMPLATE_REF &&
+    !operation.effect &&
+    !operation.refFor &&
+    operation.value.isStatic
+  ) {
+    return operation
+  }
+}

+ 2 - 0
packages/compiler-vapor/src/generators/component.ts

@@ -672,10 +672,12 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
     idMap[propsName] = null
   }
 
+  const exitSlotBlock = context.enterSlotBlock()
   let blockFn = context.withId(
     () => genBlock(oper, context, propsName ? [propsName] : []),
     idMap,
   )
+  exitSlotBlock()
   exitScope && exitScope()
 
   if (node.type === NodeTypes.ELEMENT) {

+ 21 - 1
packages/compiler-vapor/src/generators/operation.ts

@@ -12,7 +12,7 @@ import { genFor } from './for'
 import { genSetHtml } from './html'
 import { genIf } from './if'
 import { genDynamicProps, genSetProp } from './prop'
-import { genSetTemplateRef } from './templateRef'
+import { genSetTemplateRef, genSetTemplateRefBinding } from './templateRef'
 import { genGetTextChild, genSetText } from './text'
 import {
   type CodeFragment,
@@ -114,6 +114,26 @@ export function genEffects(
     varNames,
     expressionReplacements,
   } = processExpressions(context, expressions, shouldDeclare)
+  if (shouldDeclare && !declarationFrags.length && !varNames.length) {
+    const effect = effects.length === 1 ? effects[0] : undefined
+    const operation =
+      effect && effect.operations.length === 1
+        ? effect.operations[0]
+        : undefined
+    if (
+      operation &&
+      operation.type === IRNodeTypes.SET_TEMPLATE_REF &&
+      operation.effect &&
+      // Keep ref-for on the render-effect path so v-for branches reuse the
+      // root/slot-owner scoped _setTemplateRef instead of allocating a setter
+      // and its tracking WeakMaps for each item.
+      !operation.refFor
+    ) {
+      return context.withExpressionReplacements(expressionReplacements, () =>
+        context.withId(() => genSetTemplateRefBinding(operation, context), ids),
+      )
+    }
+  }
   return context.withExpressionReplacements(expressionReplacements, () => {
     push(...declarationFrags)
     for (let i = 0; i < effects.length; i++) {

+ 49 - 1
packages/compiler-vapor/src/generators/templateRef.ts

@@ -11,6 +11,11 @@ export function genSetTemplateRef(
   context: CodegenContext,
 ): CodeFragment[] {
   const [refValue, refKey] = genRefValue(oper.value, context)
+  if (context.staticTemplateRefHelperCandidate === oper) {
+    return genSetStaticTemplateRef(oper, refValue, refKey, context)
+  }
+
+  context.needsTemplateRefSetter = true
   return [
     NEWLINE,
     ...genCall(
@@ -23,7 +28,50 @@ export function genSetTemplateRef(
   ]
 }
 
-function genRefValue(value: SimpleExpressionNode, context: CodegenContext) {
+function genSetStaticTemplateRef(
+  oper: SetTemplateRefIRNode,
+  refValue: CodeFragment[],
+  refKey: string | undefined,
+  context: CodegenContext,
+): CodeFragment[] {
+  return [
+    NEWLINE,
+    ...genCall(
+      context.helper('setStaticTemplateRef'),
+      `n${oper.element}`,
+      refValue,
+      oper.refFor && 'true',
+      refKey,
+    ),
+  ]
+}
+
+export function genSetTemplateRefBinding(
+  oper: SetTemplateRefIRNode,
+  context: CodegenContext,
+): CodeFragment[] {
+  const [refValue, refKey] = genRefValue(oper.value, context)
+  const setter = context.inSlotBlock && setTemplateRefIdent
+  if (context.inSlotBlock) {
+    context.needsTemplateRefSetter = true
+  }
+  return [
+    NEWLINE,
+    ...genCall(
+      [context.helper('setTemplateRefBinding'), 'undefined'],
+      `n${oper.element}`,
+      ['() => ', ...refValue],
+      ...(setter || oper.refFor || refKey
+        ? [setter, oper.refFor && 'true', refKey]
+        : []),
+    ),
+  ]
+}
+
+function genRefValue(
+  value: SimpleExpressionNode,
+  context: CodegenContext,
+): [CodeFragment[], string?] {
   // in inline mode there is no setupState object, so we can't use string
   // keys to set the ref. Instead, we need to transform it to pass the
   // actual ref instead.

+ 233 - 0
packages/runtime-vapor/__tests__/dom/templateRef.spec.ts

@@ -6,10 +6,13 @@ import {
   createIf,
   createSlot,
   createTemplateRefSetter,
+  defineVaporAsyncComponent,
   defineVaporComponent,
   delegateEvents,
   insert,
   renderEffect,
+  setStaticTemplateRef,
+  setTemplateRefBinding,
   template,
 } from '../../src'
 import { compile, makeRender, runtimeDom, runtimeVapor } from '../_utils'
@@ -27,6 +30,7 @@ import { setElementText, setText } from '../../src/dom/prop'
 import type { VaporComponent } from '../../src/component'
 
 const define = makeRender()
+const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
 
 describe('api: template ref', () => {
   test('string ref mount', () => {
@@ -49,6 +53,26 @@ describe('api: template ref', () => {
     expect(el.value).toBe(host.children[0])
   })
 
+  test('static string ref helper mount', () => {
+    const t0 = template('<div ref="refKey"></div>')
+    const el = ref(null)
+    const { render } = define({
+      setup() {
+        return {
+          refKey: el,
+        }
+      },
+      render() {
+        const n0 = t0()
+        setStaticTemplateRef(n0 as Element, 'refKey')
+        return n0
+      },
+    })
+
+    const { host } = render()
+    expect(el.value).toBe(host.children[0])
+  })
+
   it('string ref update', async () => {
     const t0 = template('<div></div>')
     const fooEl = ref(null)
@@ -81,6 +105,35 @@ describe('api: template ref', () => {
     expect(fooEl.value).toBe(null)
   })
 
+  it('string ref binding update', async () => {
+    const t0 = template('<div></div>')
+    const fooEl = ref(null)
+    const barEl = ref(null)
+    const refKey = ref('foo')
+
+    const { render } = define({
+      setup() {
+        return {
+          foo: fooEl,
+          bar: barEl,
+        }
+      },
+      render() {
+        const n0 = t0()
+        setTemplateRefBinding(n0 as Element, () => refKey.value)
+        return n0
+      },
+    })
+    const { host } = render()
+    expect(fooEl.value).toBe(host.children[0])
+    expect(barEl.value).toBe(null)
+
+    refKey.value = 'bar'
+    await nextTick()
+    expect(barEl.value).toBe(host.children[0])
+    expect(fooEl.value).toBe(null)
+  })
+
   it('dynamic ref can be null or undefined without warning', async () => {
     const t0 = template('<div></div>')
     const el = ref(null)
@@ -197,6 +250,33 @@ describe('api: template ref', () => {
     expect(fn2.mock.calls[0][0]).toBe(host.children[0])
   })
 
+  it('function ref binding update', async () => {
+    const fn1 = vi.fn()
+    const fn2 = vi.fn()
+    const fn = ref(fn1)
+
+    const t0 = template('<div></div>')
+    const { render } = define({
+      render() {
+        const n0 = t0()
+        setTemplateRefBinding(n0 as Element, () => fn.value)
+        return n0
+      },
+    })
+
+    const { host } = render()
+
+    expect(fn1.mock.calls).toHaveLength(1)
+    expect(fn1.mock.calls[0][0]).toBe(host.children[0])
+    expect(fn2.mock.calls).toHaveLength(0)
+
+    fn.value = fn2
+    await nextTick()
+    expect(fn1.mock.calls).toHaveLength(1)
+    expect(fn2.mock.calls).toHaveLength(1)
+    expect(fn2.mock.calls[0][0]).toBe(host.children[0])
+  })
+
   it('function ref unmount', async () => {
     const fn = vi.fn()
     const toggle = ref(true)
@@ -644,6 +724,43 @@ describe('api: template ref', () => {
     expect(r.value).toBe(n)
   })
 
+  test('dynamic string ref binding inside slots', () => {
+    let childInstance: any
+    const { component: Child } = define({
+      setup() {
+        childInstance = currentInstance
+        return createSlot('default')
+      },
+    })
+
+    const r = ref()
+    const refName = ref('foo')
+    let n
+
+    const { render } = define({
+      setup() {
+        return {
+          foo: r,
+        }
+      },
+      render() {
+        const setRef = createTemplateRefSetter()
+        const n0 = createComponent(Child, null, {
+          default: () => {
+            n = document.createElement('div')
+            setTemplateRefBinding(n, () => refName.value, setRef)
+            return n
+          },
+        })
+        return n0
+      },
+    })
+
+    render()
+    expect(r.value).toBe(n)
+    expect(childInstance.refs.foo).toBeUndefined()
+  })
+
   test('inline ref inside slots', () => {
     const { component: Child } = define({
       setup() {
@@ -734,6 +851,122 @@ describe('api: template ref', () => {
     expect(html()).toBe('<div>changed</div><!--dynamic-component-->')
   })
 
+  test('component static ref updates after async resolve', async () => {
+    let resolve: (comp: VaporComponent) => void
+    const AsyncChild = defineVaporAsyncComponent(
+      () =>
+        new Promise<VaporComponent>(r => {
+          resolve = r
+        }),
+    )
+    const Child = defineVaporComponent({
+      setup(_, { expose }) {
+        expose({ name: 'async child' })
+        return template('<div>async child</div>')()
+      },
+    })
+    const foo = ref(null)
+
+    const { html } = define({
+      setup() {
+        return { foo }
+      },
+      render() {
+        const n0 = createComponent(AsyncChild)
+        setStaticTemplateRef(n0, 'foo')
+        return n0
+      },
+    }).render()
+
+    expect(foo.value).toBe(null)
+    expect(html()).toBe('<!--async component-->')
+
+    resolve!(Child)
+    await timeout()
+
+    expect(foo.value).toMatchObject({ name: 'async child' })
+    expect(html()).toBe('<div>async child</div><!--async component-->')
+  })
+
+  test('component static useTemplateRef updates after async resolve', async () => {
+    let resolve: (comp: VaporComponent) => void
+    const AsyncChild = defineVaporAsyncComponent(
+      () =>
+        new Promise<VaporComponent>(r => {
+          resolve = r
+        }),
+    )
+    const Child = defineVaporComponent({
+      setup(_, { expose }) {
+        expose({ name: 'async child' })
+        return template('<div>async child</div>')()
+      },
+    })
+    let foo: ShallowRef
+
+    const { html } = define({
+      setup() {
+        foo = useTemplateRef('foo')
+      },
+      render() {
+        const n0 = createComponent(AsyncChild)
+        setStaticTemplateRef(n0, foo!, false, 'foo')
+        return n0
+      },
+    }).render()
+
+    expect(foo!.value).toBe(null)
+    expect(html()).toBe('<!--async component-->')
+
+    resolve!(Child)
+    await timeout()
+
+    expect(foo!.value).toMatchObject({ name: 'async child' })
+    expect(html()).toBe('<div>async child</div><!--async component-->')
+  })
+
+  test('component static ref updates when switching dynamic components', async () => {
+    const One = defineVaporComponent({
+      setup(_, { expose }) {
+        expose({ name: 'one' })
+        return template('<div>one</div>')()
+      },
+    })
+    const Two = defineVaporComponent({
+      setup(_, { expose }) {
+        expose({ name: 'two' })
+        return template('<div>two</div>')()
+      },
+    })
+
+    const views: VaporComponent[] = [One, Two]
+    const view = ref(0)
+    const foo = ref<any>(null)
+
+    const { html } = define({
+      setup() {
+        return { foo }
+      },
+      render() {
+        const n0 = createDynamicComponent(() => views[view.value]) as any
+        setStaticTemplateRef(n0, 'foo')
+        return n0
+      },
+    }).render()
+
+    await nextTick()
+    const one = foo.value
+    expect(one).toMatchObject({ name: 'one' })
+    expect(html()).toBe('<div>one</div><!--dynamic-component-->')
+
+    view.value = 1
+    await nextTick()
+
+    expect(foo.value).toMatchObject({ name: 'two' })
+    expect(foo.value).not.toBe(one)
+    expect(html()).toBe('<div>two</div><!--dynamic-component-->')
+  })
+
   test('components that change their dynamics', async () => {
     const Child1 = defineVaporComponent({
       setup(_, { expose }) {

+ 40 - 4
packages/runtime-vapor/src/apiTemplateRef.ts

@@ -39,6 +39,7 @@ import {
   refCleanups,
   unsetRef,
 } from './refCleanup'
+import { renderEffect } from './renderEffect'
 
 export type NodeRef =
   | string
@@ -57,6 +58,13 @@ export type setRefFn = (
   refKey?: string,
 ) => NodeRef | undefined
 
+function getTemplateRefUpdateFragment(el: RefEl): DynamicFragment | undefined {
+  if (isDynamicFragment(el)) return el
+  if (isVaporComponent(el) && isAsyncWrapper(el)) {
+    return el.block as DynamicFragment
+  }
+}
+
 function ensureCleanup(el: RefEl): RefCleanupState {
   let cleanupRef = refCleanups.get(el)
   if (!cleanupRef) {
@@ -77,10 +85,8 @@ export function createTemplateRefSetter(): setRefFn {
 
   return (el, ref, refFor, refKey) => {
     // Re-apply refs after DynamicFragment updates.
-    if (isDynamicFragment(el) || (isVaporComponent(el) && isAsyncWrapper(el))) {
-      const frag = isDynamicFragment(el)
-        ? (el as DynamicFragment)
-        : ((el as VaporComponentInstance).block as DynamicFragment)
+    const frag = getTemplateRefUpdateFragment(el)
+    if (frag) {
       const doSet = () => {
         // KeepAlive clears refs on deactivation but keeps this fragment update
         // callback alive. Skip re-applying refs for async/offscreen updates
@@ -103,6 +109,36 @@ export function createTemplateRefSetter(): setRefFn {
   }
 }
 
+export function setStaticTemplateRef(
+  el: RefEl,
+  ref: NodeRef,
+  refFor?: boolean,
+  refKey?: string,
+): NodeRef | undefined {
+  const instance = currentInstance as VaporComponentInstance
+  const oldRef = setRef(instance, el, ref, undefined, refFor, refKey)
+  const frag = getTemplateRefUpdateFragment(el)
+  if (frag) {
+    // Static refs do not need old-ref tracking, but async/dynamic component
+    // targets still need to re-apply the same ref after their fragment updates.
+    ;(frag.onUpdated ||= []).push(() => {
+      if (isVaporComponent(el) && el.isDeactivated) return
+      setRef(instance, el, ref, oldRef, refFor, refKey)
+    })
+  }
+  return oldRef
+}
+
+export function setTemplateRefBinding(
+  el: RefEl,
+  getter: () => any,
+  setter: setRefFn = createTemplateRefSetter(),
+  refFor?: boolean,
+  refKey?: string,
+): void {
+  renderEffect(() => setter(el, getter(), refFor, refKey))
+}
+
 /**
  * Function for handling a template ref
  */

+ 5 - 1
packages/runtime-vapor/src/index.ts

@@ -68,7 +68,11 @@ export {
   getRestElement,
   getDefaultValue,
 } from './apiCreateFor'
-export { createTemplateRefSetter } from './apiTemplateRef'
+export {
+  createTemplateRefSetter,
+  setStaticTemplateRef,
+  setTemplateRefBinding,
+} from './apiTemplateRef'
 export { useVaporCssVars } from './helpers/useCssVars'
 export { setBlockKey } from './helpers/setKey'
 export { createDynamicComponent } from './apiCreateDynamicComponent'