diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
index bddd7d1d3..59deb5a00 100644
--- a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
+++ b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
@@ -20,7 +20,9 @@ import {
onBeforeUnmount,
createTextVNode,
SetupContext,
- createApp
+ createApp,
+ FunctionalComponent,
+ renderList
} from '@vue/runtime-test'
import { PatchFlags, SlotFlags } from '@vue/shared'
import { SuspenseImpl } from '../src/components/Suspense'
@@ -821,4 +823,62 @@ describe('renderer: optimized mode', () => {
await nextTick()
expect(inner(root)).toBe('
')
})
+
+ // #3881
+ // root cause: fragment inside a compiled slot passed to component which
+ // programmatically invokes the slot. The entire slot should de-opt but
+ // the fragment was incorretly put in optimized mode which causes it to skip
+ // updates for its inner components.
+ test('fragments inside programmatically invoked compiled slot should de-opt properly', async () => {
+ const Parent: FunctionalComponent = (_, { slots }) => slots.default!()
+ const Dummy = () => 'dummy'
+
+ const toggle = ref(true)
+ const force = ref(0)
+
+ const app = createApp({
+ render() {
+ if (!toggle.value) {
+ return null
+ }
+ return h(
+ Parent,
+ { n: force.value },
+ {
+ default: withCtx(
+ () => [
+ createVNode('ul', null, [
+ (openBlock(),
+ createBlock(
+ Fragment,
+ null,
+ renderList(1, item => {
+ return createVNode('li', null, [createVNode(Dummy)])
+ }),
+ 64 /* STABLE_FRAGMENT */
+ ))
+ ])
+ ],
+ undefined,
+ true
+ ),
+ _: 1 /* STABLE */
+ }
+ )
+ }
+ })
+
+ app.mount(root)
+
+ // force a patch
+ force.value++
+ await nextTick()
+ expect(inner(root)).toBe(``)
+
+ // unmount
+ toggle.value = false
+ await nextTick()
+ // should successfully unmount without error
+ expect(inner(root)).toBe(``)
+ })
})
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index e19066995..c7549ae15 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -1170,7 +1170,7 @@ function baseCreateRenderer(
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
- if (patchFlag > 0) {
+ if (dynamicChildren) {
optimized = true
}