Skip to content

fix(runtime-core): skip idle persisted transition hooks in keep-alive moves#14865

Merged
edison1105 merged 5 commits into
vuejs:mainfrom
LeSingh1:fix/14031-keep-alive-persisted-transition
May 27, 2026
Merged

fix(runtime-core): skip idle persisted transition hooks in keep-alive moves#14865
edison1105 merged 5 commits into
vuejs:mainfrom
LeSingh1:fix/14031-keep-alive-persisted-transition

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented May 20, 2026

Copy link
Copy Markdown
Contributor

Fixes #14031.

When a <keep-alive> activates or deactivates a cached child, the renderer's move function calls transition.beforeEnter / transition.enter (or leave) on every element vnode that carries a transition. For a normal transition this is correct, but for a persisted transition (the flag the template compiler injects when <Transition> has a single v-show child) the lifecycle is owned by the directive, not by mount/move. The element is being moved in or out of a detached storage container; the visibility is already controlled by v-show.

As a result, every cache hit was firing onBeforeEnter / onEnter and (because no async hook keeps it pending) onAfterEnter, even when the v-show target was never displayed. The reproduction in the issue uses a v-show="false" element inside a <keep-alive> and observes after-enter logged on each toggle.

The fix extends the same !transition.persisted guard that mountElement already uses (via the module-level needTransition helper) to the per-element branch inside move. Persisted transitions still get their hostInsert so the element is correctly relocated between the live tree and the keep-alive storage container, but the directive-owned enter / leave hooks are no longer called behind the directive's back. The existing #13153 interaction with _isLeaving lives outside the persisted path and is unaffected.

Added a regression test in BaseTransition.spec.ts that wraps a KeepAlive switching between two components inside a BaseTransition with persisted: true and asserts that none of the enter or leave hooks fire on activate or deactivate. The test fails on main (the deactivate path calls onBeforeLeave / onLeave and the activate path calls all three enter hooks via the immediate done() in BaseTransition) and passes with the change.

Summary by CodeRabbit

  • Bug Fixes

    • Persisted (cached) transitions now properly skip enter/leave lifecycle hooks when components are activated/deactivated, preserving DOM placement and avoiding unexpected animation callbacks.
    • Renderer now ensures persisted elements are inserted/removed without triggering redundant transition callbacks.
  • Tests

    • Added a test confirming cached component switches do not invoke transition lifecycle hooks.
    • Adjusted an existing transition test to account for a longer transition duration.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Renderer move() now skips enter/leave hooks for persisted transitions; a unit test verifies persisted transitions inside KeepAlive do not fire enter/leave during cached component activation/deactivation, and an e2e test's transition timing was parameterized to match a longer duration.

Changes

Persisted Transition Behavior Fix

Layer / File(s) Summary
Renderer: skip hooks for persisted moves/leaves
packages/runtime-core/src/renderer.ts
Move path: persisted transitions use hostInsert only on enter; leave path: persisted transitions call remove() directly, skipping leave and afterLeave chaining.
Test: BaseTransition #14031
packages/runtime-core/__tests__/components/BaseTransition.spec.ts
Adds a test that mounts a persisted BaseTransition inside KeepAlive, switches branches to deactivate/reactivate cached components, and asserts no enter/leave lifecycle hooks are invoked during those cycles.

E2E Transition test timing update

Layer / File(s) Summary
E2E: parameterize duration and adjust waits
packages/vue/__tests__/e2e/Transition.spec.ts
Parameterizes the browser evaluate call with duration, sets the inner Transition :duration="${duration * 4}", and updates transitionFinish() to transitionFinish(duration * 4) to align test timing with the explicit duration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

  • vuejs/core#13152: Also adjusts renderer.ts transition handling and pending leave cancellation around KeepAlive edge cases.
  • vuejs/core#14443: Addresses guards around preventing enter when a vnode is already leaving; related to enter/leave ordering and overlap fixes.

Suggested labels

scope: transition, :hammer: p3-minor-bug

Suggested reviewers

  • edison1105
  • johnsoncodehk

Poem

🐰 I hopped through branches, soft and spry,
Where kept-alive leaves no noisy cry.
Hooks stay quiet, gentle as a stream,
Cached petals rest in leafy dream.
A rabbit cheers — the tests are calm and shy.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The PR title directly and accurately summarizes the main change: fixing persisted transition hooks to skip during keep-alive moves, which is the core fix for issue #14031.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new

pkg-pr-new Bot commented May 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

@vue/compiler-core

pnpm add https://pkg.pr.new/@vue/compiler-core@14865
npm i https://pkg.pr.new/@vue/compiler-core@14865
yarn add https://pkg.pr.new/@vue/compiler-core@14865.tgz

@vue/compiler-dom

pnpm add https://pkg.pr.new/@vue/compiler-dom@14865
npm i https://pkg.pr.new/@vue/compiler-dom@14865
yarn add https://pkg.pr.new/@vue/compiler-dom@14865.tgz

@vue/compiler-sfc

pnpm add https://pkg.pr.new/@vue/compiler-sfc@14865
npm i https://pkg.pr.new/@vue/compiler-sfc@14865
yarn add https://pkg.pr.new/@vue/compiler-sfc@14865.tgz

@vue/compiler-ssr

pnpm add https://pkg.pr.new/@vue/compiler-ssr@14865
npm i https://pkg.pr.new/@vue/compiler-ssr@14865
yarn add https://pkg.pr.new/@vue/compiler-ssr@14865.tgz

@vue/reactivity

pnpm add https://pkg.pr.new/@vue/reactivity@14865
npm i https://pkg.pr.new/@vue/reactivity@14865
yarn add https://pkg.pr.new/@vue/reactivity@14865.tgz

@vue/runtime-core

pnpm add https://pkg.pr.new/@vue/runtime-core@14865
npm i https://pkg.pr.new/@vue/runtime-core@14865
yarn add https://pkg.pr.new/@vue/runtime-core@14865.tgz

@vue/runtime-dom

pnpm add https://pkg.pr.new/@vue/runtime-dom@14865
npm i https://pkg.pr.new/@vue/runtime-dom@14865
yarn add https://pkg.pr.new/@vue/runtime-dom@14865.tgz

@vue/server-renderer

pnpm add https://pkg.pr.new/@vue/server-renderer@14865
npm i https://pkg.pr.new/@vue/server-renderer@14865
yarn add https://pkg.pr.new/@vue/server-renderer@14865.tgz

@vue/shared

pnpm add https://pkg.pr.new/@vue/shared@14865
npm i https://pkg.pr.new/@vue/shared@14865
yarn add https://pkg.pr.new/@vue/shared@14865.tgz

vue

pnpm add https://pkg.pr.new/vue@14865
npm i https://pkg.pr.new/vue@14865
yarn add https://pkg.pr.new/vue@14865.tgz

@vue/compat

pnpm add https://pkg.pr.new/@vue/compat@14865
npm i https://pkg.pr.new/@vue/compat@14865
yarn add https://pkg.pr.new/@vue/compat@14865.tgz

commit: 660751e

Comment thread packages/runtime-core/src/renderer.ts Outdated
shapeFlag & ShapeFlags.ELEMENT &&
transition
transition &&
!transition.persisted

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes because the fix does address #14031, but the current blanket guard regresses the existing #13153 KeepAlive + pending v-show leave behavior.

@edison1105 edison1105 added 🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. wait changes labels May 26, 2026
Address @edison1105's review: the previous blanket `!transition.persisted`
guard on `needTransition` regressed vuejs#13153 because it routed persisted
leaves through the bare `hostInsert` fallback, skipping the
`_isLeaving` / `leaveCbKey` cancellation that vuejs#13153 added.

Restructure the move() branch so persisted transitions still take the
needTransition path (preserving the _isLeaving cancellation in
performLeave) but skip only the directive-owned beforeEnter/enter and
leave/afterLeave calls. Existing vuejs#14031 test still passes; full
runtime-core + runtime-dom suites green (1245/1246, one pre-existing
skip).
@LeSingh1

Copy link
Copy Markdown
Contributor Author

Thanks for catching that, @edison1105 — you're right, the !transition.persisted guard on needTransition routed persisted leaves through the bare hostInsert fallback and skipped the _isLeaving / leaveCbKey cancellation from #13153.

Pushed ff7a800: kept persisted transitions on the needTransition path so the if (el!._isLeaving) leaveCbKey(true) still runs in performLeave, but skipped only the directive-owned hook calls — beforeEnter / enter on MoveType.ENTER, and leave / afterLeave on MoveType.LEAVE. Persisted leaves now just do remove() after the cancellation.

The #14031 test still passes, and full runtime-core + runtime-dom suites are 1245/1246 green (one pre-existing skip). I didn't add a unit-level #13153 regression test since the existing e2e in packages/vue/__tests__/e2e/Transition.spec.ts:1846 (move kept-alive node before v-show transition leave finishes) already covers the _isLeaving / display: none invariant — let me know if you'd prefer a node-ops one too.

@edison1105 edison1105 force-pushed the fix/14031-keep-alive-persisted-transition branch from 2e53ace to 9ee1b75 Compare May 27, 2026 07:18
@edison1105 edison1105 changed the title fix(runtime-core): skip persisted transition hooks when moving kept-alive nodes (fix #14031) May 27, 2026
@edison1105 edison1105 added ready to merge The PR is ready to be merged. and removed wait changes labels May 27, 2026
@edison1105

edison1105 commented May 27, 2026

Copy link
Copy Markdown
Member

Thanks, I pushed a follow-up that keeps the #14031 persisted-transition skip narrow to cases where there is no pending v-show leave.

see 9ee1b75

@github-actions

Copy link
Copy Markdown

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 106 kB (+79 B) 40.1 kB (+33 B) 36 kB (+39 B)
vue.global.prod.js 164 kB (+79 B) 60.1 kB (+29 B) 53.4 kB (-21 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 48.8 kB (+79 B) 19 kB (+27 B) 17.4 kB (+19 B)
createApp 56.9 kB (+79 B) 22 kB (+27 B) 20.1 kB (+21 B)
createSSRApp 61.2 kB (+79 B) 23.8 kB (+27 B) 21.7 kB (+22 B)
defineCustomElement 63.1 kB (+79 B) 23.9 kB (+27 B) 21.8 kB (+35 B)
overall 71.7 kB (+79 B) 27.4 kB (+28 B) 25 kB (-10 B)
@edison1105

Copy link
Copy Markdown
Member

/ecosystem-ci run

@vuejs vuejs deleted a comment from edison1105 May 27, 2026
@vue-bot

vue-bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

📝 Ran ecosystem CI: Open

suite result latest scheduled
radix-vue success success
vitepress success success
language-tools success success
primevue success success
vite-plugin-vue success success
vueuse success success
vue-i18n success ⏹️ cancelled
vue-simple-compiler success success
router success ⏹️ cancelled
vant success success
nuxt success failure
vue-macros success success
quasar success failure
test-utils success success
pinia success success
vuetify success success
@edison1105 edison1105 merged commit 80fc139 into vuejs:main May 27, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. ready to merge The PR is ready to be merged.

3 participants