diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap index b8bef22c4..91a82db5b 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap @@ -60,7 +60,7 @@ return function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [ _createElementVNode("span", null, null, -1 /* CACHED */), - _createTextVNode("foo"), + _createTextVNode("foo", -1 /* CACHED */), _createElementVNode("div", null, null, -1 /* CACHED */) ]))) } diff --git a/packages/compiler-core/src/transforms/cacheStatic.ts b/packages/compiler-core/src/transforms/cacheStatic.ts index 239ee689a..0f112e19c 100644 --- a/packages/compiler-core/src/transforms/cacheStatic.ts +++ b/packages/compiler-core/src/transforms/cacheStatic.ts @@ -24,7 +24,13 @@ import { getVNodeHelper, } from '../ast' import type { TransformContext } from '../transform' -import { PatchFlags, isArray, isString, isSymbol } from '@vue/shared' +import { + PatchFlagNames, + PatchFlags, + isArray, + isString, + isSymbol, +} from '@vue/shared' import { findDir, isSlotOutlet } from '../utils' import { GUARD_REACTIVE_PROPS, @@ -109,6 +115,15 @@ function walk( ? ConstantTypes.NOT_CONSTANT : getConstantType(child, context) if (constantType >= ConstantTypes.CAN_CACHE) { + if ( + child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION && + child.codegenNode.arguments.length > 0 + ) { + child.codegenNode.arguments.push( + PatchFlags.CACHED + + (__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.CACHED]} */` : ``), + ) + } toCache.push(child) continue } diff --git a/packages/vue/__tests__/e2e/memory-leak.spec.ts b/packages/vue/__tests__/e2e/memory-leak.spec.ts new file mode 100644 index 000000000..2412cea2b --- /dev/null +++ b/packages/vue/__tests__/e2e/memory-leak.spec.ts @@ -0,0 +1,85 @@ +import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils' +import path from 'node:path' + +const { page, html, click } = setupPuppeteer() + +beforeEach(async () => { + await page().setContent(`
`) + await page().addScriptTag({ + path: path.resolve(__dirname, '../../dist/vue.global.js'), + }) +}) + +describe('not leaking', async () => { + // #13661 + test( + 'cached text vnodes should not retaining detached DOM nodes', + async () => { + const client = await page().createCDPSession() + await page().evaluate(async () => { + const { createApp, ref } = (window as any).Vue + createApp({ + components: { + Comp1: { + template: ` +