@reckona/mreact-compat 0.0.169 → 0.0.170

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -78,6 +78,9 @@ import {
78
78
  interface MemoFiberState {
79
79
  props: Record<string, unknown>;
80
80
  instanceKeys: string[];
81
+ hasDirtyInstanceDependencies: boolean;
82
+ hasUnflushedEffectDependencies: boolean;
83
+ hasRetainedInstanceDependencies: boolean;
81
84
  }
82
85
 
83
86
  interface FunctionFiberState {
@@ -346,6 +349,7 @@ function reconcileHostChild(
346
349
 
347
350
  const childCount = children === undefined ? 1 : children.length;
348
351
  const hasKeyedChildren = children !== undefined && hasKeyedChild(children);
352
+ const canReuseCurrentFibersInList = !hasKeyedChildren || currentFirstChild === undefined;
349
353
  let existingByKey: Map<string, Fiber> | undefined;
350
354
  let currentKeyed: Fiber | undefined = currentFirstChild;
351
355
  let currentUnkeyed = currentFirstChild;
@@ -353,29 +357,57 @@ function reconcileHostChild(
353
357
  let previous: Fiber | undefined;
354
358
  let consumed = 0;
355
359
  let skipRemainingKeyedLookup = false;
356
- const usedCurrentChildren =
357
- currentFirstChild === undefined ? undefined : new Set<Fiber>();
360
+ let sequentialCurrentCursor = currentFirstChild;
361
+ let childListOrderChanged = false;
362
+ let usedCurrentChildren =
363
+ currentFirstChild === undefined || hasKeyedChildren ? undefined : new Set<Fiber>();
364
+ const ensureUsedCurrentChildren = (): Set<Fiber> => {
365
+ if (usedCurrentChildren === undefined) {
366
+ usedCurrentChildren = new Set<Fiber>();
367
+ let cursor = currentFirstChild;
368
+
369
+ while (cursor !== undefined && cursor !== sequentialCurrentCursor) {
370
+ usedCurrentChildren.add(cursor);
371
+ cursor = cursor.sibling;
372
+ }
373
+ }
374
+
375
+ return usedCurrentChildren;
376
+ };
358
377
 
359
378
  for (let index = 0; index < childCount; index += 1) {
360
379
  const child = children === undefined ? node : children[index];
361
380
  const key = getNodeKey(child);
362
381
  let matchedCurrent: Fiber | undefined;
382
+ let canReuseMatchedCurrentFiber = canReuseCurrentFibersInList;
363
383
 
364
384
  if (key === undefined) {
385
+ if (hasKeyedChildren && currentUnkeyed !== undefined) {
386
+ childListOrderChanged = true;
387
+ ensureUsedCurrentChildren();
388
+ }
365
389
  matchedCurrent = currentUnkeyed;
366
390
  } else if (skipRemainingKeyedLookup) {
391
+ childListOrderChanged = true;
367
392
  matchedCurrent = undefined;
368
393
  } else if (existingByKey !== undefined) {
394
+ childListOrderChanged = true;
369
395
  matchedCurrent = existingByKey.get(key);
396
+ canReuseMatchedCurrentFiber = matchedCurrent === undefined;
370
397
  } else if (currentKeyed?.key === key) {
371
398
  matchedCurrent = currentKeyed;
399
+ canReuseMatchedCurrentFiber = true;
372
400
  currentKeyed = currentKeyed.sibling;
401
+ sequentialCurrentCursor = currentKeyed;
373
402
  } else if (
374
403
  children !== undefined &&
375
404
  currentKeyed?.sibling?.key === key &&
376
405
  canSkipSingleDeletedKeyedFiber(children, index, currentKeyed.sibling)
377
406
  ) {
407
+ childListOrderChanged = true;
408
+ ensureUsedCurrentChildren();
378
409
  matchedCurrent = currentKeyed.sibling;
410
+ canReuseMatchedCurrentFiber = false;
379
411
  currentKeyed = currentKeyed.sibling.sibling;
380
412
  } else {
381
413
  if (
@@ -383,19 +415,30 @@ function reconcileHostChild(
383
415
  hasKeyedChildren &&
384
416
  canSkipRemainingKeyedLookup(currentKeyed, children, index)
385
417
  ) {
418
+ childListOrderChanged = true;
386
419
  skipRemainingKeyedLookup = true;
387
420
  currentKeyed = undefined;
388
421
  } else if (hasKeyedChildren) {
422
+ childListOrderChanged = true;
423
+ ensureUsedCurrentChildren();
389
424
  existingByKey = collectExistingKeyedFibers(currentKeyed);
390
425
  matchedCurrent = existingByKey.get(key);
426
+ canReuseMatchedCurrentFiber = matchedCurrent === undefined;
391
427
  }
392
428
  }
393
429
 
430
+ const memoBailout = tryReuseDependencyFreeMemoBailout(
431
+ matchedCurrent,
432
+ child,
433
+ runtime,
434
+ options,
435
+ canReuseMatchedCurrentFiber,
436
+ );
394
437
  const previousNodes =
395
- options.previousNodes === undefined
438
+ memoBailout !== undefined || options.previousNodes === undefined
396
439
  ? undefined
397
440
  : options.previousNodes.slice(consumed);
398
- const result = createHostFiber(
441
+ const result = memoBailout ?? createHostFiber(
399
442
  parent,
400
443
  matchedCurrent,
401
444
  child,
@@ -403,6 +446,7 @@ function reconcileHostChild(
403
446
  runtime,
404
447
  getReconcileChildPath(path, child, index, options),
405
448
  previousNodes === undefined ? options : { ...options, previousNodes },
449
+ canReuseMatchedCurrentFiber,
406
450
  );
407
451
  const fiber = result.fiber;
408
452
 
@@ -449,8 +493,13 @@ function reconcileHostChild(
449
493
  previous = fiber;
450
494
  }
451
495
 
452
- markUnusedCurrentChildrenForDeletion(parent, currentFirstChild, usedCurrentChildren);
453
- parent.childListChanged = childFiberListShapeChanged(currentFirstChild, first);
496
+ if (usedCurrentChildren === undefined && hasKeyedChildren && currentKeyed !== undefined) {
497
+ markOptimizedChildrenForDeletion(parent, currentKeyed);
498
+ } else {
499
+ markUnusedCurrentChildrenForDeletion(parent, currentFirstChild, usedCurrentChildren);
500
+ }
501
+ parent.childListChanged =
502
+ childListOrderChanged || childFiberListShapeChanged(currentFirstChild, first);
454
503
 
455
504
  return { fiber: first, consumed };
456
505
  }
@@ -465,6 +514,7 @@ function reconcileKeyedRowHostChildren(
465
514
  children.length === 0 ||
466
515
  currentFirstChild === undefined ||
467
516
  options.previousNodes !== undefined ||
517
+ !isKeyedRowHostElementCandidate(children[0]) ||
468
518
  !shouldUseDirectHostTextChild()
469
519
  ) {
470
520
  return undefined;
@@ -681,6 +731,15 @@ function createKeyedRowHostElementScratch(): KeyedRowHostElement {
681
731
  };
682
732
  }
683
733
 
734
+ function isKeyedRowHostElementCandidate(node: ReactCompatNode): boolean {
735
+ return (
736
+ isReactCompatElement(node) &&
737
+ typeof node.type === "string" &&
738
+ node.key !== null &&
739
+ node.ref === null
740
+ );
741
+ }
742
+
684
743
  function readKeyedRowHostElement(
685
744
  node: ReactCompatNode,
686
745
  row: KeyedRowHostElement,
@@ -958,8 +1017,18 @@ function createHostFiber(
958
1017
  runtime: RootRuntime | undefined,
959
1018
  path: string,
960
1019
  options: FiberHydrationOptions = {},
1020
+ canReuseCurrentFiber = true,
961
1021
  ): FiberReconcileResult {
962
- const result = createHostFiberImpl(parent, current, node, key, runtime, path, options);
1022
+ const result = createHostFiberImpl(
1023
+ parent,
1024
+ current,
1025
+ node,
1026
+ key,
1027
+ runtime,
1028
+ path,
1029
+ options,
1030
+ canReuseCurrentFiber,
1031
+ );
963
1032
 
964
1033
  if (result.fiber !== undefined) {
965
1034
  if (canFinalizeNewHostFiber(result.fiber, current, node, options)) {
@@ -1000,6 +1069,7 @@ function createHostFiberImpl(
1000
1069
  runtime: RootRuntime | undefined,
1001
1070
  path: string,
1002
1071
  options: FiberHydrationOptions = {},
1072
+ canReuseCurrentFiber = true,
1003
1073
  ): FiberReconcileResult {
1004
1074
  if (node === null || node === undefined || typeof node === "boolean") {
1005
1075
  return { fiber: undefined, consumed: 0 };
@@ -1052,6 +1122,18 @@ function createHostFiberImpl(
1052
1122
  return { fiber, consumed: childResult.consumed };
1053
1123
  }
1054
1124
 
1125
+ const memoBailout = tryReuseMemoBailout(
1126
+ current,
1127
+ node,
1128
+ runtime,
1129
+ path,
1130
+ options,
1131
+ canReuseCurrentFiber,
1132
+ );
1133
+ if (memoBailout !== undefined) {
1134
+ return memoBailout;
1135
+ }
1136
+
1055
1137
  if (!isReactCompatElement(node)) {
1056
1138
  if (isReactCompatPortal(node)) {
1057
1139
  return createPortalFiber(parent, current, node, key, runtime, path, options);
@@ -1246,10 +1328,7 @@ function createHostFiberImpl(
1246
1328
  forwardRefType,
1247
1329
  );
1248
1330
  fiber.memoizedState = getDevToolsHookState(runtime, path);
1249
- const childOptions = withHydrationComponentStack(
1250
- options,
1251
- getComponentName(forwardRefType.render),
1252
- );
1331
+ const childOptions = getHydrationChildOptions(options, forwardRefType.render);
1253
1332
  const childResult = reconcileHostChild(
1254
1333
  fiber,
1255
1334
  current?.tag === "forward-ref" ? current.child : undefined,
@@ -1269,28 +1348,47 @@ function createHostFiberImpl(
1269
1348
 
1270
1349
  const memoType = node.type;
1271
1350
  const memoPath = `${path}.memo`;
1351
+ const previousMemoFiber =
1352
+ current?.tag === "memo" && current.type === memoType ? current : undefined;
1272
1353
  const previousMemoState =
1273
- current?.tag === "memo"
1274
- ? (current.memoizedState as MemoFiberState | undefined)
1354
+ previousMemoFiber !== undefined
1355
+ ? (previousMemoFiber.memoizedState as MemoFiberState | undefined)
1275
1356
  : undefined;
1276
- const fiber =
1277
- current?.tag === "memo" && current.type === memoType
1278
- ? createWorkInProgress(current, node.props)
1279
- : createFiber("memo", node.props, key);
1280
- fiber.type = memoType;
1281
1357
 
1282
1358
  if (
1359
+ previousMemoFiber !== undefined &&
1283
1360
  previousMemoState !== undefined &&
1284
- !hasDirtyInstance(runtime, previousMemoState.instanceKeys, memoPath) &&
1285
- !hasUnflushedMountEffectInstance(runtime, previousMemoState.instanceKeys) &&
1361
+ !(
1362
+ memoStateNeedsDirtyInstanceCheck(previousMemoState) &&
1363
+ hasDirtyInstance(runtime, previousMemoState.instanceKeys, memoPath)
1364
+ ) &&
1365
+ !(
1366
+ memoStateNeedsEffectCheck(previousMemoState) &&
1367
+ hasUnflushedMountEffectInstance(runtime, previousMemoState.instanceKeys)
1368
+ ) &&
1286
1369
  areMemoPropsEqual(memoType, previousMemoState.props, node.props)
1287
1370
  ) {
1288
- markActiveInstanceKeys(runtime, previousMemoState.instanceKeys);
1289
- fiber.child = getSkippedChild(current);
1371
+ const fiber = getMemoBailoutFiber(
1372
+ runtime,
1373
+ previousMemoFiber,
1374
+ node.props,
1375
+ previousMemoState,
1376
+ canReuseCurrentFiber,
1377
+ );
1378
+ fiber.child = getSkippedChild(previousMemoFiber);
1290
1379
  fiber.memoizedState = previousMemoState;
1291
- return { fiber, consumed: options.previousNodes?.length ?? 0 };
1380
+ return {
1381
+ fiber,
1382
+ consumed: options.previousNodes?.length ?? 0,
1383
+ };
1292
1384
  }
1293
1385
 
1386
+ const fiber =
1387
+ previousMemoFiber !== undefined
1388
+ ? createWorkInProgress(previousMemoFiber, node.props)
1389
+ : createFiber("memo", node.props, key);
1390
+ fiber.type = memoType;
1391
+
1294
1392
  const renderedElement: ReactCompatElement = {
1295
1393
  ...node,
1296
1394
  type: memoType.type,
@@ -1310,9 +1408,19 @@ function createHostFiberImpl(
1310
1408
  fiber.child.sibling = undefined;
1311
1409
  bubbleHostChild(fiber, fiber.child);
1312
1410
  }
1411
+ const instanceKeys = collectInstanceKeys(runtime, memoPath);
1412
+ const hasClassDescendant = hasClassComponentDescendant(fiber.child);
1313
1413
  fiber.memoizedState = {
1314
- props: { ...node.props },
1315
- instanceKeys: collectInstanceKeys(runtime, memoPath),
1414
+ props: node.props as Record<string, unknown>,
1415
+ instanceKeys,
1416
+ hasDirtyInstanceDependencies:
1417
+ hasDirtyInstanceDependencies(runtime, instanceKeys) || hasClassDescendant,
1418
+ hasUnflushedEffectDependencies: hasUnflushedEffectDependencies(
1419
+ runtime,
1420
+ instanceKeys,
1421
+ ),
1422
+ hasRetainedInstanceDependencies:
1423
+ hasRetainedInstanceDependencies(runtime, instanceKeys) || hasClassDescendant,
1316
1424
  };
1317
1425
  return { fiber, consumed: childResult.consumed };
1318
1426
  }
@@ -1422,10 +1530,7 @@ function createHostFiberImpl(
1422
1530
  return { fiber, consumed: options.previousNodes?.length ?? 0 };
1423
1531
  }
1424
1532
 
1425
- const childOptions = withHydrationComponentStack(
1426
- options,
1427
- getComponentName(classType),
1428
- );
1533
+ const childOptions = getHydrationChildOptions(options, classType);
1429
1534
 
1430
1535
  try {
1431
1536
  const childResult = reconcileHostChild(
@@ -1507,10 +1612,7 @@ function createHostFiberImpl(
1507
1612
  node.type,
1508
1613
  );
1509
1614
  fiber.memoizedState = getDevToolsHookState(runtime, path);
1510
- const childOptions = withHydrationComponentStack(
1511
- options,
1512
- getComponentName(node.type as Function),
1513
- );
1615
+ const childOptions = getHydrationChildOptions(options, node.type as Function);
1514
1616
  const childResult = reconcileHostChild(
1515
1617
  fiber,
1516
1618
  current?.tag === "function-component" ? current.child : undefined,
@@ -2082,6 +2184,26 @@ function collectCommittedHostNodes(fiber: Fiber): Node[] {
2082
2184
  return nodes;
2083
2185
  }
2084
2186
 
2187
+ function hasCommittedHostNode(fiber: Fiber): boolean {
2188
+ if (
2189
+ (fiber.tag === "host-component" || fiber.tag === "host-text") &&
2190
+ fiber.stateNode instanceof Node
2191
+ ) {
2192
+ return true;
2193
+ }
2194
+
2195
+ let child = fiber.child;
2196
+
2197
+ while (child !== undefined) {
2198
+ if (hasCommittedHostNode(child)) {
2199
+ return true;
2200
+ }
2201
+ child = child.sibling;
2202
+ }
2203
+
2204
+ return false;
2205
+ }
2206
+
2085
2207
  function getSkippedChild(current: Fiber | undefined): Fiber | undefined {
2086
2208
  const child = current?.child;
2087
2209
  const alternateChild = current?.alternate?.child;
@@ -2089,8 +2211,8 @@ function getSkippedChild(current: Fiber | undefined): Fiber | undefined {
2089
2211
  if (
2090
2212
  child !== undefined &&
2091
2213
  alternateChild !== undefined &&
2092
- collectCommittedHostNodes(child).length === 0 &&
2093
- collectCommittedHostNodes(alternateChild).length > 0
2214
+ !hasCommittedHostNode(child) &&
2215
+ hasCommittedHostNode(alternateChild)
2094
2216
  ) {
2095
2217
  return alternateChild;
2096
2218
  }
@@ -2980,6 +3102,104 @@ function hasPendingAsyncChild(fiber: Fiber | undefined): boolean {
2980
3102
  return false;
2981
3103
  }
2982
3104
 
3105
+ function tryReuseMemoBailout(
3106
+ current: Fiber | undefined,
3107
+ node: ReactCompatNode,
3108
+ runtime: RootRuntime | undefined,
3109
+ path: string,
3110
+ options: FiberHydrationOptions,
3111
+ canReuseCurrentFiber = true,
3112
+ ): FiberReconcileResult | undefined {
3113
+ if (
3114
+ current?.tag !== "memo" ||
3115
+ runtime === undefined ||
3116
+ !isReactCompatElement(node) ||
3117
+ node.type !== current.type ||
3118
+ !isMemoType(node.type)
3119
+ ) {
3120
+ return undefined;
3121
+ }
3122
+
3123
+ const previousMemoState = current.memoizedState as MemoFiberState | undefined;
3124
+ const memoPath = `${path}.memo`;
3125
+
3126
+ if (
3127
+ previousMemoState === undefined ||
3128
+ (
3129
+ memoStateNeedsDirtyInstanceCheck(previousMemoState) &&
3130
+ hasDirtyInstance(runtime, previousMemoState.instanceKeys, memoPath)
3131
+ ) ||
3132
+ (
3133
+ memoStateNeedsEffectCheck(previousMemoState) &&
3134
+ hasUnflushedMountEffectInstance(runtime, previousMemoState.instanceKeys)
3135
+ ) ||
3136
+ !areMemoPropsEqual(node.type, previousMemoState.props, node.props)
3137
+ ) {
3138
+ return undefined;
3139
+ }
3140
+
3141
+ const fiber = getMemoBailoutFiber(
3142
+ runtime,
3143
+ current,
3144
+ node.props,
3145
+ previousMemoState,
3146
+ canReuseCurrentFiber,
3147
+ );
3148
+ fiber.type = node.type;
3149
+ fiber.child = getSkippedChild(current);
3150
+ fiber.memoizedState = previousMemoState;
3151
+ return {
3152
+ fiber,
3153
+ consumed: options.previousNodes?.length ?? 0,
3154
+ };
3155
+ }
3156
+
3157
+ function tryReuseDependencyFreeMemoBailout(
3158
+ current: Fiber | undefined,
3159
+ node: ReactCompatNode,
3160
+ runtime: RootRuntime | undefined,
3161
+ options: FiberHydrationOptions,
3162
+ canReuseCurrentFiber: boolean,
3163
+ ): FiberReconcileResult | undefined {
3164
+ if (
3165
+ options.previousNodes !== undefined ||
3166
+ current?.tag !== "memo" ||
3167
+ runtime === undefined ||
3168
+ !isReactCompatElement(node) ||
3169
+ node.type !== current.type ||
3170
+ !isMemoType(node.type)
3171
+ ) {
3172
+ return undefined;
3173
+ }
3174
+
3175
+ const previousMemoState = current.memoizedState as MemoFiberState | undefined;
3176
+
3177
+ if (
3178
+ previousMemoState === undefined ||
3179
+ previousMemoState.hasDirtyInstanceDependencies !== false ||
3180
+ previousMemoState.hasUnflushedEffectDependencies !== false ||
3181
+ previousMemoState.hasRetainedInstanceDependencies !== false ||
3182
+ !areMemoPropsEqual(node.type, previousMemoState.props, node.props)
3183
+ ) {
3184
+ return undefined;
3185
+ }
3186
+
3187
+ const fiber = getMemoBailoutFiber(
3188
+ runtime,
3189
+ current,
3190
+ node.props,
3191
+ previousMemoState,
3192
+ canReuseCurrentFiber,
3193
+ );
3194
+ fiber.type = node.type;
3195
+ fiber.child = getSkippedChild(current);
3196
+ fiber.memoizedState = previousMemoState;
3197
+ return {
3198
+ fiber,
3199
+ consumed: 0,
3200
+ };
3201
+ }
3202
+
2983
3203
  function isPendingLazyFiber(fiber: Fiber): boolean {
2984
3204
  if (fiber.tag !== "lazy" || !isLazyType(fiber.type)) {
2985
3205
  return false;
@@ -3223,6 +3443,15 @@ function joinCommitPath(path: string, segment: string): string {
3223
3443
  return path === SKIP_COMMIT_PATH ? "" : joinPath(path, segment);
3224
3444
  }
3225
3445
 
3446
+ function getHydrationChildOptions(
3447
+ options: FiberHydrationOptions,
3448
+ component: Function,
3449
+ ): FiberHydrationOptions {
3450
+ return options.hydration === undefined
3451
+ ? options
3452
+ : withHydrationComponentStack(options, getComponentName(component));
3453
+ }
3454
+
3226
3455
  function getComponentName(component: Function): string {
3227
3456
  return component.name === "" ? "Anonymous" : component.name;
3228
3457
  }
@@ -3288,6 +3517,152 @@ function markActiveInstanceKeys(runtime: RootRuntime, keys: readonly string[]):
3288
3517
  }
3289
3518
  }
3290
3519
 
3520
+ type RuntimeHookSlotLike = {
3521
+ kind?: string;
3522
+ };
3523
+
3524
+ type RuntimeEffectHookSlotLike = RuntimeHookSlotLike & {
3525
+ mounted?: boolean;
3526
+ disposed?: boolean;
3527
+ };
3528
+
3529
+ interface RuntimeInstanceLike {
3530
+ hooks?: readonly (RuntimeHookSlotLike | undefined)[];
3531
+ contextDependencies?: unknown;
3532
+ }
3533
+
3534
+ function memoStateNeedsDirtyInstanceCheck(state: MemoFiberState): boolean {
3535
+ return state.hasDirtyInstanceDependencies !== false;
3536
+ }
3537
+
3538
+ function memoStateNeedsEffectCheck(state: MemoFiberState): boolean {
3539
+ return state.hasUnflushedEffectDependencies !== false;
3540
+ }
3541
+
3542
+ function memoStateNeedsActiveInstanceMark(state: MemoFiberState): boolean {
3543
+ return state.hasRetainedInstanceDependencies !== false;
3544
+ }
3545
+
3546
+ function getMemoBailoutFiber(
3547
+ runtime: RootRuntime,
3548
+ current: Fiber,
3549
+ pendingProps: unknown,
3550
+ state: MemoFiberState,
3551
+ canReuseCurrentFiber: boolean,
3552
+ ): Fiber {
3553
+ if (canReuseCurrentFiber && canReuseMemoBailoutFiber(current, state)) {
3554
+ current.pendingProps = pendingProps;
3555
+ current.flags = NoFlags;
3556
+ current.subtreeFlags = NoFlags;
3557
+ current.childListChanged = false;
3558
+ current.subtreeChildListChanged = false;
3559
+ current.hostChildListChanged = false;
3560
+ return current;
3561
+ }
3562
+
3563
+ const fiber = createWorkInProgress(current, pendingProps);
3564
+ if (memoStateNeedsActiveInstanceMark(state)) {
3565
+ markActiveInstanceKeys(runtime, state.instanceKeys);
3566
+ }
3567
+ return fiber;
3568
+ }
3569
+
3570
+ function canReuseMemoBailoutFiber(current: Fiber, state: MemoFiberState): boolean {
3571
+ return (
3572
+ state.hasRetainedInstanceDependencies === false &&
3573
+ current.hasRefSubtree !== true &&
3574
+ current.hydrateExisting !== true
3575
+ );
3576
+ }
3577
+
3578
+ function hasDirtyInstanceDependencies(
3579
+ runtime: RootRuntime,
3580
+ keys: readonly string[],
3581
+ ): boolean {
3582
+ for (const key of keys) {
3583
+ const instance = runtime.instances.get(key) as RuntimeInstanceLike | undefined;
3584
+
3585
+ if (instance === undefined || instance.contextDependencies !== undefined) {
3586
+ return true;
3587
+ }
3588
+
3589
+ if (instance.hooks?.some(isDirtyCapableHookSlot) === true) {
3590
+ return true;
3591
+ }
3592
+ }
3593
+
3594
+ return false;
3595
+ }
3596
+
3597
+ function hasRetainedInstanceDependencies(
3598
+ runtime: RootRuntime,
3599
+ keys: readonly string[],
3600
+ ): boolean {
3601
+ for (const key of keys) {
3602
+ const instance = runtime.instances.get(key) as RuntimeInstanceLike | undefined;
3603
+
3604
+ if (instance === undefined || instance.contextDependencies !== undefined) {
3605
+ return true;
3606
+ }
3607
+
3608
+ if (instance.hooks !== undefined && instance.hooks.length > 0) {
3609
+ return true;
3610
+ }
3611
+ }
3612
+
3613
+ return false;
3614
+ }
3615
+
3616
+ function isDirtyCapableHookSlot(slot: RuntimeHookSlotLike | undefined): boolean {
3617
+ if (slot === undefined) {
3618
+ return false;
3619
+ }
3620
+
3621
+ return (
3622
+ slot.kind !== "ref" &&
3623
+ slot.kind !== "memo" &&
3624
+ slot.kind !== "debug" &&
3625
+ slot.kind !== "effect"
3626
+ );
3627
+ }
3628
+
3629
+ function hasUnflushedEffectDependencies(
3630
+ runtime: RootRuntime,
3631
+ keys: readonly string[],
3632
+ ): boolean {
3633
+ for (const key of keys) {
3634
+ const instance = runtime.instances.get(key) as RuntimeInstanceLike | undefined;
3635
+
3636
+ if (instance === undefined) {
3637
+ return true;
3638
+ }
3639
+
3640
+ if (instance.hooks?.some((slot) => slot?.kind === "effect") === true) {
3641
+ return true;
3642
+ }
3643
+ }
3644
+
3645
+ return false;
3646
+ }
3647
+
3648
+ function hasClassComponentDescendant(fiber: Fiber | undefined): boolean {
3649
+ let cursor = fiber;
3650
+
3651
+ while (cursor !== undefined) {
3652
+ if (cursor.tag === "class-component") {
3653
+ return true;
3654
+ }
3655
+
3656
+ if (hasClassComponentDescendant(cursor.child)) {
3657
+ return true;
3658
+ }
3659
+
3660
+ cursor = cursor.sibling;
3661
+ }
3662
+
3663
+ return false;
3664
+ }
3665
+
3291
3666
  function hasDirtyInstance(
3292
3667
  runtime: RootRuntime | undefined,
3293
3668
  keys: readonly string[],
@@ -3301,6 +3676,10 @@ function hasDirtyInstance(
3301
3676
  return true;
3302
3677
  }
3303
3678
 
3679
+ if (keys.length === 0) {
3680
+ return false;
3681
+ }
3682
+
3304
3683
  if (keys.some(
3305
3684
  (key) =>
3306
3685
  (runtime.instances.get(key) as { dirty?: boolean } | undefined)?.dirty === true,
@@ -3312,12 +3691,21 @@ function hasDirtyInstance(
3312
3691
  return false;
3313
3692
  }
3314
3693
 
3315
- for (const [key, instance] of runtime.instances) {
3316
- if (
3317
- (key === prefix || key.startsWith(`${prefix}.`)) &&
3318
- (instance as { dirty?: boolean }).dirty === true
3319
- ) {
3320
- return true;
3694
+ // Resolve dirty descendants through the prefix index instead of scanning
3695
+ // every runtime instance. The previous full-map scan made memo/function
3696
+ // bailout O(total instances) per node, i.e. O(n^2) for large keyed lists
3697
+ // (js-framework-benchmark update-every-10th / select). The index is the same
3698
+ // source of truth used by collectRuntimeInstanceKeys.
3699
+ const keysUnderPrefix = runtime.instanceKeysByPrefix.get(prefix);
3700
+
3701
+ if (keysUnderPrefix !== undefined) {
3702
+ for (const key of keysUnderPrefix) {
3703
+ if (
3704
+ (runtime.instances.get(key) as { dirty?: boolean } | undefined)?.dirty ===
3705
+ true
3706
+ ) {
3707
+ return true;
3708
+ }
3321
3709
  }
3322
3710
  }
3323
3711
 
@@ -3327,7 +3715,7 @@ function hasDirtyInstance(
3327
3715
  function hasUnflushedMountEffectInstance(runtime: RootRuntime, keys: readonly string[]): boolean {
3328
3716
  return keys.some((key) => {
3329
3717
  const instance = runtime.instances.get(key) as
3330
- | { hooks?: readonly ({ kind?: string; mounted?: boolean; disposed?: boolean } | undefined)[] }
3718
+ | { hooks?: readonly (RuntimeEffectHookSlotLike | undefined)[] }
3331
3719
  | undefined;
3332
3720
 
3333
3721
  return instance?.hooks?.some(