diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
index 26fee92f..73e566c9 100644
--- a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
+++ b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts
@@ -11,9 +11,12 @@ import {
   serializeInner as inner,
   VNode,
   ref,
-  nextTick
+  nextTick,
+  defineComponent,
+  withCtx,
+  renderSlot
 } from '@vue/runtime-test'
-import { PatchFlags } from '@vue/shared'
+import { PatchFlags, SlotFlags } from '@vue/shared'
 
 describe('renderer: optimized mode', () => {
   let root: TestElement
@@ -398,4 +401,52 @@ describe('renderer: optimized mode', () => {
     expect(inner(root)).toBe('
bar
')
     expect(block!.dynamicChildren).toBe(null)
   })
+
+  // #1980
+  test('dynamicChildren should be tracked correctly when normalizing slots to plain children', async () => {
+    let block: VNode
+    const Comp = defineComponent({
+      setup(_props, { slots }) {
+        return () => {
+          const vnode = (openBlock(),
+          (block = createBlock('div', null, {
+            default: withCtx(() => [renderSlot(slots, 'default')]),
+            _: SlotFlags.FORWARDED
+          })))
+
+          return vnode
+        }
+      }
+    })
+
+    const foo = ref(0)
+    const App = {
+      setup() {
+        return () => {
+          return createVNode(Comp, null, {
+            default: withCtx(() => [
+              createVNode('p', null, foo.value, PatchFlags.TEXT)
+            ]),
+            // Indicates that this is a stable slot to avoid bail out
+            _: SlotFlags.STABLE
+          })
+        }
+      }
+    }
+
+    render(h(App), root)
+    expect(inner(root)).toBe('')
+    expect(block!.dynamicChildren!.length).toBe(1)
+    expect(block!.dynamicChildren![0].type).toBe(Fragment)
+    expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
+    expect(
+      serialize(block!.dynamicChildren![0].dynamicChildren![0]
+        .el as TestElement)
+    ).toBe('0
')
+
+    foo.value++
+    await nextTick()
+
+    expect(inner(root)).toBe('')
+  })
 })
diff --git a/packages/runtime-core/__tests__/vnode.spec.ts b/packages/runtime-core/__tests__/vnode.spec.ts
index ad357561..2ee1872a 100644
--- a/packages/runtime-core/__tests__/vnode.spec.ts
+++ b/packages/runtime-core/__tests__/vnode.spec.ts
@@ -130,10 +130,10 @@ describe('vnode', () => {
     })
 
     test('object', () => {
-      const vnode = createVNode('p', null, { foo: 'foo' })
+      const vnode = createVNode({}, null, { foo: 'foo' })
       expect(vnode.children).toMatchObject({ foo: 'foo' })
       expect(vnode.shapeFlag).toBe(
-        ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
+        ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.SLOTS_CHILDREN
       )
     })
 
diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts
index be6dbba4..2e43c628 100644
--- a/packages/runtime-core/src/helpers/renderSlot.ts
+++ b/packages/runtime-core/src/helpers/renderSlot.ts
@@ -11,6 +11,8 @@ import { PatchFlags, SlotFlags } from '@vue/shared'
 import { warn } from '../warning'
 
 export let isRenderingCompiledSlot = 0
+export const setCompiledSlotRendering = (n: number) =>
+  (isRenderingCompiledSlot += n)
 
 /**
  * Compiler runtime helper for rendering ``
diff --git a/packages/runtime-core/src/helpers/withRenderContext.ts b/packages/runtime-core/src/helpers/withRenderContext.ts
index 4ac273f5..88a29ae3 100644
--- a/packages/runtime-core/src/helpers/withRenderContext.ts
+++ b/packages/runtime-core/src/helpers/withRenderContext.ts
@@ -16,7 +16,7 @@ export function withCtx(
   ctx: ComponentInternalInstance | null = currentRenderingInstance
 ) {
   if (!ctx) return fn
-  return function renderFnWithContext() {
+  const renderFnWithContext = (...args: any[]) => {
     // If a user calls a compiled slot inside a template expression (#1745), it
     // can mess up block tracking, so by default we need to push a null block to
     // avoid that. This isn't necessary if rendering a compiled ``.
@@ -25,11 +25,13 @@ export function withCtx(
     }
     const owner = currentRenderingInstance
     setCurrentRenderingInstance(ctx)
-    const res = fn.apply(null, arguments as any)
+    const res = fn(...args)
     setCurrentRenderingInstance(owner)
     if (!isRenderingCompiledSlot) {
       closeBlock()
     }
     return res
   }
+  renderFnWithContext._c = true
+  return renderFnWithContext
 }
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index 0f20a534..de329ff9 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -36,6 +36,7 @@ import { currentRenderingInstance } from './componentRenderUtils'
 import { RendererNode, RendererElement } from './renderer'
 import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
 import { hmrDirtyComponents } from './hmr'
+import { setCompiledSlotRendering } from './helpers/renderSlot'
 
 export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
   __isFragment: true
@@ -539,12 +540,15 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
   } else if (isArray(children)) {
     type = ShapeFlags.ARRAY_CHILDREN
   } else if (typeof children === 'object') {
-    // Normalize slot to plain children
-    if (
-      (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) &&
-      (children as any).default
-    ) {
-      normalizeChildren(vnode, (children as any).default())
+    if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
+      // Normalize slot to plain children for plain element and Teleport
+      const slot = (children as any).default
+      if (slot) {
+        // _c marker is added by withCtx() indicating this is a compiled slot
+        slot._c && setCompiledSlotRendering(1)
+        normalizeChildren(vnode, slot())
+        slot._c && setCompiledSlotRendering(-1)
+      }
       return
     } else {
       type = ShapeFlags.SLOTS_CHILDREN