Node.normalize() removes Fragment text anchors and causes subsequent updates to fail
Vue version
3.5.42
Link to minimal reproduction
Steps to reproduce
- Open the reproduction link (Vue 3.5.42, DEV mode, SSR off).
- Click "1. Normalize body" to execute document.body.normalize() inside the preview frame.
- Click "2. Toggle branch".
- Observe the nextSibling error: First/Second remain visible instead of Replacement branch.
Verified control: without calling normalize(), Toggle branch switches to Replacement branch and back successfully.
What is expected?
Node.normalize() removes empty text nodes and merges adjacent text nodes. Vue Fragment boundaries use empty text nodes, so normalizing an ancestor may remove these boundaries while the VNodes retain references to the original nodes.
Would it be possible to preserve compatibility between Node.normalize() and Fragment anchors?
What is actually happening?
The issue is reproducible. After clicking Normalize body and then Toggle branch, the update fails: First/Second remain visible instead of Replacement branch. Without normalization, switching in both directions works.
Error stack:
TypeError: Cannot read properties of null (reading 'nextSibling')
at nextSibling (http://127.0.0.1:5179/node_modules/.vite/deps/vue_dist_vue__esm-bundler__js.js?v=895c4d76:7607:30)
at removeFragment (http://127.0.0.1:5179/node_modules/.vite/deps/vue_dist_vue__esm-bundler__js.js?v=895c4d76:6128:11)
at remove (http://127.0.0.1:5179/node_modules/.vite/deps/vue_dist_vue__esm-bundler__js.js?v=895c4d76:6107:9)
at unmount (http://127.0.0.1:5179/node_modules/.vite/deps/vue_dist_vue__esm-bundler__js.js?v=895c4d76:6091:18)
at patch (http://127.0.0.1:5179/node_modules/.vite/deps/vue_dist_vue__esm-bundler__js.js?v=895c4d76:5503:4)
at patchBlockChildren (http://127.0.0.1:5179/node_modules/.vite/deps/vue_dist_vue__esm-bundler__js.js?v=895c4d76:5696:4)
at patchElement (http://127.0.0.1:5179/node_modules/.vite/deps/vue_dist_vue__esm-bundler__js.js?v=895c4d76:5662:4)
at processElement (http://127.0.0.1:5179/node_modules/.vite/deps/vue_dist_vue__esm-bundler__js.js?v=895c4d76:5584:5)
at patch (http://127.0.0.1:5179/node_modules/.vite/deps/vue_dist_vue__esm-bundler__js.js?v=895c4d76:5525:32)
at ReactiveEffect.componentUpdateFn [as fn] (http://127.0.0.1:5179/node_modules/.vite/deps/vue_dist_vue__esm-bundler__js.js?v=895c4d76:5852:5)
System Info
System:
OS: macOS 15.5
CPU: (12) arm64 Apple M3 Pro
Memory: 326.52 MB / 36.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 22.22.2 - /Users/<redacted>/.asdf/installs/nodejs/22.22.2/bin/node
Yarn: 1.22.22 - /Users/<redacted>/.asdf/installs/nodejs/22.22.2/bin/yarn
npm: 10.9.7 - /Users/<redacted>/.asdf/plugins/nodejs/shims/npm
pnpm: 10.33.2 - /Users/<redacted>/.asdf/installs/nodejs/22.22.2/bin/pnpm
bun: 1.3.13 - /Users/<redacted>/.asdf/shims/bun
Deno: 2.7.9 - /opt/homebrew/bin/deno
npmPackages:
vue: 3.5.42 => 3.5.42
Any additional comments?
Related reports:
- https://github.com/opencloud-eu/web/issues/2966 explicitly reports mark.js calling Node.normalize(), removing Vue v-for/v-if Fragment anchors and causing an insertBefore error.
- https://github.com/opencloud-eu/web/pull/3253 replaces mark.js with render-based highlighting.
- https://github.com/vuejs/core/issues/12330 explains that Fragment text anchors are intentional. This report concerns their compatibility with DOM normalization.
A simplified workaround skips normalize() calls on the application root and its ancestors:
const originalNormalize = Node.prototype.normalize;
Node.prototype.normalize = function () {
const appRoot = document.getElementById('app');
if (appRoot && this.contains(appRoot)) return;
return originalNormalize.call(this);
};
Source: vuejs/core