fix(compiler-vapor): drop comment children of Transition/TransitionGroup
Project: vuejs/core | Language: TypeScript | Concepts: refactoring, testing-strategy, error-handling, api-design | Difficulty: mid
Link: https://github.com/vuejs/core/pull/15374
The problem
When using <transition> or <transition-group> components in Vue, comment nodes (like <!-- comment -->) were not filtered out during compilation in "vapor" mode. This led to inconsistencies between client-rendered, server-rendered (SSR), and hydrated DOM output, causing possible hydration mismatches and unnecessary nodes in the DOM.
Why it's non-trivial
A naive fix might just ignore comment nodes everywhere, but comments are sometimes needed (e.g., for v-if/else branches or template slots). The filtering must only apply to comment nodes that are direct children of Transition or TransitionGroup components, and must match both the virtual DOM (vdom) runtime and SSR behavior. The fix must also ensure that hydration (matching the SSR output to the client) works without warnings or mismatches.
The approach
1. Add logic to filter out comment children for Transition/TransitionGroup in the compiler:
+ // Transition/TransitionGroup filter out comment children at runtime in
+ // vdom mode and SSR omits them (#5351, #11961), so drop them here to keep
+ // client render and hydration in sync with the server output (#15372)
+ const isTransitionHost = isTransitionNode(node) || isTransitionGroupNode(node)
This computes whether the current node is a <transition> or <transition-group> component.
- if (n.type === NodeTypes.COMMENT && hasTemplateSlots) {
+ if (
+ n.type === NodeTypes.COMMENT &&
+ (hasTemplateSlots || isTransitionHost)
+ ) {
ignoreComment(n, context)
return false
}
This change ensures that comment nodes are ignored (not included in the output) if they are children of a transition/transition-group or if template slots are present.
2. Add compiler and hydration tests to verify the behavior:
Compiler test:
+ test('should drop comment children to match vdom runtime and SSR behavior', () => {
+ // vdom runtime filters comment children of Transition/TransitionGroup and
+ // SSR omits them (#5351, #11961) - vapor drops them at compile time
+ const { code } = compile(
+ `<transition>
+ <!-- comment -->
+ <div>foo</div>
+ </transition>`,
+ { prefixIdentifiers: true },
+ )
+ expect(code).not.toContain('comment')
+ expect(code).toMatchSnapshot()
+
+ const { code: groupCode } = compile(
+ `<transition-group tag="ul">
+ <!-- comment -->
+ <li key="a">a</li>
+ <li key="b">b</li>
+ </transition-group>`,
+ { prefixIdentifiers: true },
+ )
+ expect(groupCode).not.toContain('comment')
+ expect(groupCode).toMatchSnapshot()
+ })
This test checks that the generated code for transitions and transition-groups does not include comment nodes.
Hydration test:
+ test('should drop comment children to stay in sync with SSR output', async () => {
+ const data = ref({
+ items: ['a', 'b', 'c'],
+ })
+ const code = `
+ <TransitionGroup :css="false" tag="ul">
+ <!-- comment -->
+ <li v-for="item in data.items" :key="item">{{ item }}</li>
+ </TransitionGroup>
+ `
+ const SSRComp = compileVaporComponent(code, data, undefined, true)
+ const html = await VueServerRenderer.renderToString(
+ runtimeDom.createSSRApp(SSRComp),
+ )
+ // SSR deliberately omits comment children of TransitionGroup (#11961)
+ expect(html).not.toContain('comment')
+
+ const { container } = await mountWithHydration(html, code, data)
+ const ul = container.querySelector('ul')!
+
+ expect(formatHtml(ul.innerHTML)).toMatchInlineSnapshot(
+ `"<li>a</li><li>b</li><li>c</li><!--for-->"`,
+ )
+ expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+ expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+ expect(
+ `Hydration completed but contains mismatches.`,
+ ).not.toHaveBeenWarned()
+
+ data.value.items.push('d')
+ await nextTick()
+ expect(formatHtml(ul.innerHTML)).toMatchInlineSnapshot(
+ `"<li>a</li><li>b</li><li>c</li><li>d</li><!--for-->"`,
+ )
+ })
This test verifies that SSR output omits comments, hydration completes without warnings, and dynamic updates work as expected.
From the review thread
No notable review discussion on this PR.
Takeaways
- Match runtime and SSR output in the compiler to avoid hydration mismatches.
- Only filter or drop nodes (like comments) in context-specific situations, not globally.
- Always add regression tests for both compilation and hydration when fixing SSR/client consistency bugs.