@legendapp/list 3.3.0 → 3.3.1

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 3.3.1
2
+
3
+ - Perf: Animated `scrollToIndex` and `scrollToOffset` calls now mount the destination rows before native scrolling starts, so long programmatic jumps are less likely to show blank space at the target.
4
+ - Perf: Fast scrolling shifts more of the render buffer in the direction of travel and settles back after scrolling stops, so rows ahead of the user are ready sooner without keeping the buffer displaced afterward.
5
+ - Perf: Scroll velocity favors recent movement and ignores stale samples, preventing old scroll events from keeping the render buffer pointed in the wrong direction after scrolling slows or stops.
6
+ - Perf: On Fabric, row measurements wait for all expected layout-effect measurements before recalculating positions, reducing the number of computations while scrolling quickly.
7
+
1
8
  ## 3.3.0
2
9
 
3
10
  - Feat: Add `dataKey` so apps can tell the list when the current array represents a different dataset, such as switching conversations or feeds without remounting `LegendList`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/list",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
4
4
  "description": "Legend List is a drop-in replacement for FlatList with much better performance and supporting dynamically sized items.",
5
5
  "sideEffects": false,
6
6
  "private": false,
package/react-native.js CHANGED
@@ -1534,6 +1534,7 @@ function finishScrollTo(ctx) {
1534
1534
  const scrollingTo = state.scrollingTo;
1535
1535
  state.scrollHistory.length = 0;
1536
1536
  state.scrollingTo = void 0;
1537
+ state.scrollTargetPinnedRange = void 0;
1537
1538
  if (state.pendingTotalSize !== void 0) {
1538
1539
  addTotalSize(ctx, null, state.pendingTotalSize);
1539
1540
  }
@@ -2173,6 +2174,8 @@ var flushSync = (fn) => {
2173
2174
  };
2174
2175
 
2175
2176
  // src/utils/getScrollVelocity.ts
2177
+ var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3;
2178
+ var SCROLL_VELOCITY_HALF_LIFE_MS = 200;
2176
2179
  var getScrollVelocity = (state) => {
2177
2180
  const { scrollHistory } = state;
2178
2181
  const newestIndex = scrollHistory.length - 1;
@@ -2180,35 +2183,37 @@ var getScrollVelocity = (state) => {
2180
2183
  return 0;
2181
2184
  }
2182
2185
  const newest = scrollHistory[newestIndex];
2183
- const now = Date.now();
2184
- let direction = 0;
2185
- for (let i = newestIndex; i > 0; i--) {
2186
- const delta = scrollHistory[i].scroll - scrollHistory[i - 1].scroll;
2187
- if (delta !== 0) {
2188
- direction = Math.sign(delta);
2189
- break;
2190
- }
2191
- }
2192
- if (direction === 0) {
2186
+ if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) {
2193
2187
  return 0;
2194
2188
  }
2195
- let oldest = newest;
2196
- for (let i = newestIndex - 1; i >= 0; i--) {
2189
+ let direction = 0;
2190
+ let weightedVelocity = 0;
2191
+ let totalWeight = 0;
2192
+ for (let i = newestIndex; i > 0; i--) {
2197
2193
  const current = scrollHistory[i];
2198
- const next = scrollHistory[i + 1];
2199
- const delta = next.scroll - current.scroll;
2200
- const deltaSign = Math.sign(delta);
2201
- if (deltaSign !== 0 && deltaSign !== direction) {
2202
- break;
2194
+ const previous = scrollHistory[i - 1];
2195
+ const scrollDiff = current.scroll - previous.scroll;
2196
+ const timeDiff = current.time - previous.time;
2197
+ const deltaSign = Math.sign(scrollDiff);
2198
+ if (deltaSign !== 0) {
2199
+ if (direction === 0) {
2200
+ direction = deltaSign;
2201
+ } else if (deltaSign !== direction) {
2202
+ break;
2203
+ }
2203
2204
  }
2204
- if (now - current.time > 1e3) {
2205
+ if (newest.time - previous.time > MAX_SCROLL_VELOCITY_WINDOW_MS) {
2205
2206
  break;
2206
2207
  }
2207
- oldest = current;
2208
+ if (scrollDiff === 0 || timeDiff <= 0) {
2209
+ continue;
2210
+ }
2211
+ const age = newest.time - current.time;
2212
+ const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS);
2213
+ weightedVelocity += scrollDiff / timeDiff * weight;
2214
+ totalWeight += weight;
2208
2215
  }
2209
- const scrollDiff = newest.scroll - oldest.scroll;
2210
- const timeDiff = newest.time - oldest.time;
2211
- return timeDiff > 0 ? scrollDiff / timeDiff : 0;
2216
+ return totalWeight > 0 ? weightedVelocity / totalWeight : 0;
2212
2217
  };
2213
2218
 
2214
2219
  // src/core/updateScroll.ts
@@ -2325,8 +2330,93 @@ function syncInitialScrollNativeWatchdog(state, options) {
2325
2330
  initialScrollWatchdog.clear(state);
2326
2331
  }
2327
2332
  }
2328
- function scrollTo(ctx, params) {
2333
+ function findPositionIndexAtOrBeforeOffset(ctx, offset) {
2334
+ const state = ctx.state;
2335
+ const dataLength = state.props.data.length;
2336
+ let low = 0;
2337
+ let high = dataLength - 1;
2338
+ let match;
2339
+ while (low <= high) {
2340
+ const mid = Math.floor((low + high) / 2);
2341
+ const top = state.positions[mid];
2342
+ if (top === void 0) {
2343
+ high = mid - 1;
2344
+ } else {
2345
+ if (top <= offset) {
2346
+ match = mid;
2347
+ low = mid + 1;
2348
+ } else {
2349
+ high = mid - 1;
2350
+ }
2351
+ }
2352
+ }
2353
+ return match;
2354
+ }
2355
+ function getItemBottom(ctx, index) {
2329
2356
  var _a3;
2357
+ const top = ctx.state.positions[index];
2358
+ if (top === void 0) {
2359
+ return void 0;
2360
+ }
2361
+ const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0;
2362
+ return top + (Number.isFinite(itemSize) ? itemSize : 0);
2363
+ }
2364
+ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) {
2365
+ const state = ctx.state;
2366
+ const dataLength = state.props.data.length;
2367
+ if (dataLength === 0) {
2368
+ return void 0;
2369
+ }
2370
+ const viewportStart = Math.max(0, targetOffset);
2371
+ const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength);
2372
+ let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart);
2373
+ if (start === void 0) {
2374
+ return void 0;
2375
+ }
2376
+ if (targetIndex !== void 0 && state.positions[start] === void 0) {
2377
+ return { end: start, start };
2378
+ }
2379
+ if (targetIndex === void 0) {
2380
+ const startBottom = getItemBottom(ctx, start);
2381
+ if (startBottom === void 0 || startBottom <= viewportStart) {
2382
+ return void 0;
2383
+ }
2384
+ }
2385
+ while (start > 0) {
2386
+ const top = state.positions[start];
2387
+ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) {
2388
+ break;
2389
+ }
2390
+ start--;
2391
+ }
2392
+ while (start > 0) {
2393
+ const previousBottom = getItemBottom(ctx, start - 1);
2394
+ if (previousBottom === void 0 || previousBottom <= viewportStart) {
2395
+ break;
2396
+ }
2397
+ start--;
2398
+ }
2399
+ let end = start;
2400
+ while (end + 1 < dataLength) {
2401
+ const nextTop = state.positions[end + 1];
2402
+ if (nextTop === void 0 || nextTop > viewportEnd) {
2403
+ break;
2404
+ }
2405
+ end++;
2406
+ }
2407
+ return { end, start };
2408
+ }
2409
+ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) {
2410
+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex);
2411
+ if (range) {
2412
+ ctx.state.scrollTargetPinnedRange = range;
2413
+ ctx.state.scrollForNextCalculateItemsInView = void 0;
2414
+ } else {
2415
+ ctx.state.scrollTargetPinnedRange = void 0;
2416
+ }
2417
+ }
2418
+ function scrollTo(ctx, params) {
2419
+ var _a3, _b;
2330
2420
  const state = ctx.state;
2331
2421
  const { noScrollingTo, forceScroll, ...scrollTarget } = params;
2332
2422
  const {
@@ -2361,11 +2451,20 @@ function scrollTo(ctx, params) {
2361
2451
  targetOffset,
2362
2452
  waitForInitialScrollCompletionFrame
2363
2453
  };
2454
+ if (!isInitialScroll) {
2455
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
2456
+ }
2364
2457
  }
2365
2458
  state.scrollPending = targetOffset;
2366
2459
  syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset });
2367
- if (!animated && !isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) {
2368
- updateScroll(ctx, targetOffset, false, { markHasScrolled: false });
2460
+ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) {
2461
+ if (animated) {
2462
+ if (state.scrollTargetPinnedRange) {
2463
+ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state);
2464
+ }
2465
+ } else {
2466
+ updateScroll(ctx, targetOffset, true, { markHasScrolled: false });
2467
+ }
2369
2468
  }
2370
2469
  if (forceScroll || !isInitialScroll || Platform.OS === "android") {
2371
2470
  doScrollTo(ctx, { animated, horizontal, isInitialScroll, offset });
@@ -4037,6 +4136,32 @@ function setDidLayout(ctx) {
4037
4136
  }
4038
4137
 
4039
4138
  // src/core/calculateItemsInView.ts
4139
+ var RENDER_RANGE_PROJECTION_FULL_VELOCITY = 4;
4140
+ var RENDER_RANGE_PROJECTION_SETTLE_DELAY = 100;
4141
+ function getProjectedBufferAdjustment(scrollVelocity, trailingBuffer) {
4142
+ if (trailingBuffer <= 0) {
4143
+ return 0;
4144
+ }
4145
+ const velocityProgress = Math.min(1, Math.abs(scrollVelocity) / RENDER_RANGE_PROJECTION_FULL_VELOCITY);
4146
+ return Math.sign(scrollVelocity) * trailingBuffer * velocityProgress;
4147
+ }
4148
+ function scheduleRenderRangeProjectionSettle(ctx) {
4149
+ const state = ctx.state;
4150
+ const previousTimeout = state.timeoutRenderRangeProjectionSettle;
4151
+ if (previousTimeout !== void 0) {
4152
+ clearTimeout(previousTimeout);
4153
+ state.timeouts.delete(previousTimeout);
4154
+ }
4155
+ const timeout = setTimeout(() => {
4156
+ var _a3;
4157
+ state.timeoutRenderRangeProjectionSettle = void 0;
4158
+ state.timeouts.delete(timeout);
4159
+ state.scrollHistory.length = 0;
4160
+ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
4161
+ }, RENDER_RANGE_PROJECTION_SETTLE_DELAY);
4162
+ state.timeoutRenderRangeProjectionSettle = timeout;
4163
+ state.timeouts.add(timeout);
4164
+ }
4040
4165
  function findCurrentStickyIndex(stickyArray, scroll, state) {
4041
4166
  const positions = state.positions;
4042
4167
  for (let i = stickyArray.length - 1; i >= 0; i--) {
@@ -4077,14 +4202,14 @@ function handleStickyActivation(ctx, stickyArray, currentStickyIdx, needNewConta
4077
4202
  }
4078
4203
  }
4079
4204
  }
4080
- function handleStickyRecycling(ctx, stickyArray, scroll, drawDistance, currentStickyIdx, pendingRemoval, alwaysRenderIndicesSet) {
4205
+ function handleStickyRecycling(ctx, stickyArray, scroll, drawDistance, currentStickyIdx, pendingRemoval, isPinnedRenderIndex) {
4081
4206
  var _a3, _b;
4082
4207
  const state = ctx.state;
4083
4208
  for (const containerIndex of state.stickyContainerPool) {
4084
4209
  const itemKey = peek$(ctx, `containerItemKey${containerIndex}`);
4085
4210
  const itemIndex = itemKey ? state.indexByKey.get(itemKey) : void 0;
4086
4211
  if (itemIndex === void 0) continue;
4087
- if (alwaysRenderIndicesSet.has(itemIndex)) continue;
4212
+ if (isPinnedRenderIndex(itemIndex)) continue;
4088
4213
  const arrayIdx = stickyArray.indexOf(itemIndex);
4089
4214
  if (arrayIdx === -1) {
4090
4215
  state.stickyContainerPool.delete(containerIndex);
@@ -4248,8 +4373,6 @@ function calculateItemsInView(ctx, params = {}) {
4248
4373
  const { data } = state.props;
4249
4374
  const stickyHeaderIndicesArr = state.props.stickyHeaderIndicesArr || [];
4250
4375
  const stickyHeaderIndicesSet = state.props.stickyHeaderIndicesSet || /* @__PURE__ */ new Set();
4251
- const alwaysRenderArr = alwaysRenderIndicesArr || [];
4252
- const alwaysRenderSet = alwaysRenderIndicesSet || /* @__PURE__ */ new Set();
4253
4376
  const drawDistance = getEffectiveDrawDistance(ctx, params.drawDistanceMode);
4254
4377
  const { dataChanged, doMVCP, forceFullItemPositions } = params;
4255
4378
  const bootstrapInitialScrollState = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
@@ -4316,14 +4439,19 @@ function calculateItemsInView(ctx, params = {}) {
4316
4439
  scrollBufferTop = drawDistance * 1.5;
4317
4440
  scrollBufferBottom = drawDistance * 0.5;
4318
4441
  }
4442
+ const shouldProjectRenderRange = !dataChanged && !forceFullItemPositions && !suppressInitialScrollSideEffects && !hasActiveInitialScroll(state) && !state.scrollingTo && !state.pendingNativeMVCPAdjust && !!peek$(ctx, "readyToRender");
4443
+ const projectedBufferAdjustment = shouldProjectRenderRange ? getProjectedBufferAdjustment(speed, Math.min(scrollBufferTop, scrollBufferBottom)) : 0;
4319
4444
  const updateScrollRange = () => {
4320
4445
  const scrollStart = Math.max(0, scroll);
4321
4446
  const overscrollBeforeContent = Math.max(0, -nativeScrollState);
4322
- scrollTopBuffered = scrollStart - scrollBufferTop;
4323
4447
  scrollBottom = Math.max(scrollStart, scroll + scrollLength + overscrollBeforeContent);
4324
- scrollBottomBuffered = scrollBottom + scrollBufferBottom;
4448
+ scrollTopBuffered = scrollStart - scrollBufferTop + projectedBufferAdjustment;
4449
+ scrollBottomBuffered = scrollBottom + scrollBufferBottom + projectedBufferAdjustment;
4325
4450
  };
4326
4451
  updateScrollRange();
4452
+ if (projectedBufferAdjustment !== 0) {
4453
+ scheduleRenderRangeProjectionSettle(ctx);
4454
+ }
4327
4455
  if (enableScrollForNextCalculateItemsInView && !suppressInitialScrollSideEffects && !dataChanged && !forceFullItemPositions && scrollForNextCalculateItemsInView) {
4328
4456
  const { top, bottom } = scrollForNextCalculateItemsInView;
4329
4457
  if (top === null && bottom === null) {
@@ -4481,9 +4609,34 @@ function calculateItemsInView(ctx, params = {}) {
4481
4609
  }
4482
4610
  }
4483
4611
  }
4612
+ const scrollTargetPinnedRange = state.scrollTargetPinnedRange;
4613
+ let scrollTargetPinnedStart = 0;
4614
+ let scrollTargetPinnedEnd = -1;
4615
+ if (scrollTargetPinnedRange) {
4616
+ scrollTargetPinnedStart = Math.max(0, Math.min(scrollTargetPinnedRange.start, scrollTargetPinnedRange.end));
4617
+ scrollTargetPinnedEnd = Math.min(
4618
+ dataLength - 1,
4619
+ Math.max(scrollTargetPinnedRange.start, scrollTargetPinnedRange.end)
4620
+ );
4621
+ }
4622
+ const hasScrollTargetPinnedRange = scrollTargetPinnedStart <= scrollTargetPinnedEnd;
4623
+ const isPinnedRenderIndex = (index) => alwaysRenderIndicesSet.has(index) || hasScrollTargetPinnedRange && index >= scrollTargetPinnedStart && index <= scrollTargetPinnedEnd;
4484
4624
  if (startBuffered !== null && endBuffered !== null) {
4485
4625
  const needNewContainers = [];
4486
4626
  const needNewContainersSet = /* @__PURE__ */ new Set();
4627
+ const addPinnedIndex = (index) => {
4628
+ var _a4;
4629
+ if (index >= 0 && index < dataLength) {
4630
+ const id = (_a4 = idCache[index]) != null ? _a4 : getId(state, index);
4631
+ const containerIndex = containerItemKeys.get(id);
4632
+ if (containerIndex !== void 0) {
4633
+ state.stickyContainerPool.add(containerIndex);
4634
+ } else if (!isNullOrUndefined(id) && !needNewContainersSet.has(index)) {
4635
+ needNewContainersSet.add(index);
4636
+ needNewContainers.push(index);
4637
+ }
4638
+ }
4639
+ };
4487
4640
  for (let i = startBuffered; i <= endBuffered; i++) {
4488
4641
  const id = (_m = idCache[i]) != null ? _m : getId(state, i);
4489
4642
  if (!containerItemKeys.has(id)) {
@@ -4491,21 +4644,19 @@ function calculateItemsInView(ctx, params = {}) {
4491
4644
  needNewContainers.push(i);
4492
4645
  }
4493
4646
  }
4494
- if (alwaysRenderArr.length > 0) {
4495
- for (const index of alwaysRenderArr) {
4496
- if (index < 0 || index >= dataLength) continue;
4497
- const id = (_n = idCache[index]) != null ? _n : getId(state, index);
4498
- if (id && !containerItemKeys.has(id) && !needNewContainersSet.has(index)) {
4499
- needNewContainersSet.add(index);
4500
- needNewContainers.push(index);
4501
- }
4647
+ for (const index of alwaysRenderIndicesArr) {
4648
+ addPinnedIndex(index);
4649
+ }
4650
+ if (hasScrollTargetPinnedRange) {
4651
+ for (let index = scrollTargetPinnedStart; index <= scrollTargetPinnedEnd; index++) {
4652
+ addPinnedIndex(index);
4502
4653
  }
4503
4654
  }
4504
4655
  if (stickyHeaderIndicesArr.length > 0) {
4505
4656
  handleStickyActivation(
4506
4657
  ctx,
4507
4658
  stickyHeaderIndicesArr,
4508
- (_o = stickyState == null ? void 0 : stickyState.currentStickyIdx) != null ? _o : -1,
4659
+ (_n = stickyState == null ? void 0 : stickyState.currentStickyIdx) != null ? _n : -1,
4509
4660
  needNewContainers,
4510
4661
  needNewContainersSet,
4511
4662
  startBuffered,
@@ -4531,7 +4682,7 @@ function calculateItemsInView(ctx, params = {}) {
4531
4682
  for (const allocation of availableContainerAllocations) {
4532
4683
  const i = allocation.itemIndex;
4533
4684
  const containerIndex = allocation.containerIndex;
4534
- const id = (_p = idCache[i]) != null ? _p : getId(state, i);
4685
+ const id = (_o = idCache[i]) != null ? _o : getId(state, i);
4535
4686
  const oldKey = peek$(ctx, `containerItemKey${containerIndex}`);
4536
4687
  if (oldKey && oldKey !== id) {
4537
4688
  containerItemKeys.delete(oldKey);
@@ -4542,10 +4693,14 @@ function calculateItemsInView(ctx, params = {}) {
4542
4693
  state.containerItemTypes.set(containerIndex, allocation.itemType);
4543
4694
  }
4544
4695
  containerItemKeys.set(id, containerIndex);
4545
- (_q = state.userScrollAnchorReset) == null ? void 0 : _q.keys.add(id);
4696
+ (_p = state.userScrollAnchorReset) == null ? void 0 : _p.keys.add(id);
4697
+ if (IsNewArchitecture) {
4698
+ (_q = state.pendingLayoutEffectMeasurements) != null ? _q : state.pendingLayoutEffectMeasurements = /* @__PURE__ */ new Set();
4699
+ state.pendingLayoutEffectMeasurements.add(id);
4700
+ }
4546
4701
  const containerSticky = `containerSticky${containerIndex}`;
4547
4702
  const isSticky = stickyHeaderIndicesSet.has(i);
4548
- const isAlwaysRender = alwaysRenderSet.has(i);
4703
+ const isPinnedRender = isPinnedRenderIndex(i);
4549
4704
  if (isSticky) {
4550
4705
  set$(ctx, containerSticky, true);
4551
4706
  state.stickyContainerPool.add(containerIndex);
@@ -4553,9 +4708,9 @@ function calculateItemsInView(ctx, params = {}) {
4553
4708
  if (peek$(ctx, containerSticky)) {
4554
4709
  set$(ctx, containerSticky, false);
4555
4710
  }
4556
- if (isAlwaysRender) {
4711
+ if (isPinnedRender) {
4557
4712
  state.stickyContainerPool.add(containerIndex);
4558
- } else if (state.stickyContainerPool.has(containerIndex)) {
4713
+ } else {
4559
4714
  state.stickyContainerPool.delete(containerIndex);
4560
4715
  }
4561
4716
  }
@@ -4570,20 +4725,8 @@ function calculateItemsInView(ctx, params = {}) {
4570
4725
  }
4571
4726
  }
4572
4727
  }
4573
- if (state.userScrollAnchorReset) {
4574
- if (state.userScrollAnchorReset.keys.size === 0) {
4575
- state.userScrollAnchorReset = void 0;
4576
- }
4577
- }
4578
- if (alwaysRenderArr.length > 0) {
4579
- for (const index of alwaysRenderArr) {
4580
- if (index < 0 || index >= dataLength) continue;
4581
- const id = (_r = idCache[index]) != null ? _r : getId(state, index);
4582
- const containerIndex = containerItemKeys.get(id);
4583
- if (containerIndex !== void 0) {
4584
- state.stickyContainerPool.add(containerIndex);
4585
- }
4586
- }
4728
+ if (((_r = state.userScrollAnchorReset) == null ? void 0 : _r.keys.size) === 0) {
4729
+ state.userScrollAnchorReset = void 0;
4587
4730
  }
4588
4731
  }
4589
4732
  if (state.stickyContainerPool.size > 0) {
@@ -4594,7 +4737,7 @@ function calculateItemsInView(ctx, params = {}) {
4594
4737
  drawDistance,
4595
4738
  (_s = stickyState == null ? void 0 : stickyState.currentStickyIdx) != null ? _s : -1,
4596
4739
  pendingRemoval,
4597
- alwaysRenderSet
4740
+ isPinnedRenderIndex
4598
4741
  );
4599
4742
  }
4600
4743
  const pendingRemovalSet = pendingRemoval.length > 0 ? new Set(pendingRemoval) : void 0;
@@ -4766,6 +4909,36 @@ function mergeItemSizeUpdateResult(result, next) {
4766
4909
  result.needsRecalculate || (result.needsRecalculate = next.needsRecalculate);
4767
4910
  result.shouldMaintainScrollAtEnd || (result.shouldMaintainScrollAtEnd = next.shouldMaintainScrollAtEnd);
4768
4911
  }
4912
+ var batchedItemSizeRecalculates = /* @__PURE__ */ new WeakMap();
4913
+ function flushBatchedItemSizeRecalculate(ctx, didFallback = false, expectedResult) {
4914
+ const result = batchedItemSizeRecalculates.get(ctx);
4915
+ if (!result || expectedResult && result !== expectedResult) {
4916
+ return;
4917
+ }
4918
+ batchedItemSizeRecalculates.delete(ctx);
4919
+ if (didFallback) {
4920
+ ctx.state.pendingLayoutEffectMeasurements = void 0;
4921
+ }
4922
+ flushItemSizeUpdates(ctx, result);
4923
+ }
4924
+ function queueItemSizeRecalculate(ctx, result) {
4925
+ var _a3, _b;
4926
+ const batch = batchedItemSizeRecalculates.get(ctx);
4927
+ if (batch) {
4928
+ mergeItemSizeUpdateResult(batch, result);
4929
+ } else {
4930
+ const nextBatch = { ...result };
4931
+ batchedItemSizeRecalculates.set(ctx, nextBatch);
4932
+ if ((_a3 = ctx.state.pendingLayoutEffectMeasurements) == null ? void 0 : _a3.size) {
4933
+ requestAnimationFrame(() => {
4934
+ flushBatchedItemSizeRecalculate(ctx, true, nextBatch);
4935
+ });
4936
+ }
4937
+ }
4938
+ if (!((_b = ctx.state.pendingLayoutEffectMeasurements) == null ? void 0 : _b.size)) {
4939
+ flushBatchedItemSizeRecalculate(ctx);
4940
+ }
4941
+ }
4769
4942
  function flushItemSizeUpdates(ctx, result) {
4770
4943
  var _a3;
4771
4944
  const state = ctx.state;
@@ -4782,12 +4955,22 @@ function flushItemSizeUpdates(ctx, result) {
4782
4955
  function updateItemSizes(ctx, measurement) {
4783
4956
  var _a3, _b, _c, _d;
4784
4957
  const state = ctx.state;
4958
+ let didDrainLayoutEffectMeasurements = false;
4959
+ if (IsNewArchitecture && measurement.fromLayoutEffect) {
4960
+ const pendingLayoutEffectMeasurements = state.pendingLayoutEffectMeasurements;
4961
+ if ((pendingLayoutEffectMeasurements == null ? void 0 : pendingLayoutEffectMeasurements.delete(measurement.itemKey)) && pendingLayoutEffectMeasurements.size === 0) {
4962
+ state.pendingLayoutEffectMeasurements = void 0;
4963
+ didDrainLayoutEffectMeasurements = true;
4964
+ }
4965
+ }
4785
4966
  const pendingKeys = (_a3 = state.userScrollAnchorReset) == null ? void 0 : _a3.keys;
4786
4967
  const shouldBatchPendingMeasurements = IsNewArchitecture && measurement.fromLayoutEffect && !!(pendingKeys == null ? void 0 : pendingKeys.size);
4968
+ const shouldQueueRecalculate = IsNewArchitecture && !!measurement.fromLayoutEffect;
4969
+ let result;
4787
4970
  if (!shouldBatchPendingMeasurements) {
4788
- flushItemSizeUpdates(ctx, applyItemSize(ctx, measurement.itemKey, measurement.size));
4971
+ result = applyItemSize(ctx, measurement.itemKey, measurement.size);
4789
4972
  } else {
4790
- const result = {};
4973
+ result = {};
4791
4974
  const updateContainerItemSize = (itemKey, containerId, size) => {
4792
4975
  if (containerId !== void 0 && peek$(ctx, `containerItemKey${containerId}`) === itemKey) {
4793
4976
  mergeItemSizeUpdateResult(result, applyItemSize(ctx, itemKey, size));
@@ -4805,8 +4988,15 @@ function updateItemSizes(ctx, measurement) {
4805
4988
  });
4806
4989
  }
4807
4990
  }
4991
+ }
4992
+ if (shouldQueueRecalculate && result.needsRecalculate) {
4993
+ queueItemSizeRecalculate(ctx, result);
4994
+ } else {
4808
4995
  flushItemSizeUpdates(ctx, result);
4809
4996
  }
4997
+ if (didDrainLayoutEffectMeasurements) {
4998
+ flushBatchedItemSizeRecalculate(ctx);
4999
+ }
4810
5000
  }
4811
5001
  function applyItemSize(ctx, itemKey, sizeObj) {
4812
5002
  var _a3;