mirror of
https://github.com/react/react-native.git
synced 2026-09-28 13:23:09 +08:00
Summary: `VirtualizedList`/`FlatList`/`SectionList` re-run a small amount of bookkeeping on every render and on every scroll event (60–120 Hz on ProMotion displays). This PR removes three allocations from those hot paths without changing any observable behavior: 1. **`VirtualizedList.render` no longer builds a `Set` for `stickyHeaderIndices` on every render** when the prop is not provided (the common case). The Set is now only created when the prop is present; the two `.has()` lookups use optional access. 2. **`ChildListCollection.forEach` returns early when there are no nested child lists** (the common case) instead of allocating a `Map.values()` iterator. This is called from `_onScroll` and the four other scroll callbacks on every scroll event. 3. **`_orientation()` caches its result** and only rebuilds the object when the `horizontal` prop changes. `I18nManager.isRTL` is a module-load constant (only changes on app reload), so the cache is invalidated solely by the `horizontal` prop. The object is replaced, never mutated, which keeps `ListMetricsAggregator`'s field-based invalidation correct. ## Changelog: [GENERAL][CHANGED] - Reduce allocations in the `VirtualizedList` render and scroll path (avoid per-render `Set` allocation for `stickyHeaderIndices`, per-scroll-event `Map` iterator for the empty nested-list collection, and per-call `orientation` object allocation) Pull Request resolved: https://github.com/react/react-native/pull/58593 Test Plan: - `yarn test packages/virtualized-lists` → 9 suites, 186 passed, 69 snapshots: - `ChildListCollection-test.js` (new): forEach over populated/empty collection, removal, `forEachInCell`/`anyInCell` - `VirtualizedList-test.js`: `stickyHeaderIndices` not forwarded when the prop is absent (with `ListHeaderComponent`), forwarded when provided; orientation cache identity + invalidation on `horizontal` change - `yarn flow-check` → 0 errors - `yarn lint` → 0 errors, 0 warnings - `yarn format-check` (changed files) Micro-benchmark (Node v24, V8, 2M iterations, before vs after, same machine; the real-world benefit is dominated by reduced GC pressure, which is largest on low-end Android): Reviewed By: Abbondanzo Differential Revision: D121177373 Pulled By: javache fbshipit-source-id: 1befd7e6d90fbd1fcea9c4cc8cf6d575c6c26111
78 lines
2.1 KiB
JavaScript
78 lines
2.1 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow strict
|
|
* @format
|
|
*/
|
|
|
|
import invariant from 'invariant';
|
|
|
|
export default class ChildListCollection<TList> {
|
|
_cellKeyToChildren: Map<string, Set<TList>> = new Map();
|
|
_childrenToCellKey: Map<TList, string> = new Map();
|
|
|
|
add(list: TList, cellKey: string): void {
|
|
invariant(
|
|
!this._childrenToCellKey.has(list),
|
|
'Trying to add already present child list',
|
|
);
|
|
|
|
const cellLists = this._cellKeyToChildren.get(cellKey) ?? new Set();
|
|
cellLists.add(list);
|
|
this._cellKeyToChildren.set(cellKey, cellLists);
|
|
|
|
this._childrenToCellKey.set(list, cellKey);
|
|
}
|
|
|
|
remove(list: TList): void {
|
|
const cellKey = this._childrenToCellKey.get(list);
|
|
invariant(cellKey != null, 'Trying to remove non-present child list');
|
|
this._childrenToCellKey.delete(list);
|
|
|
|
const cellLists = this._cellKeyToChildren.get(cellKey);
|
|
invariant(cellLists, '_cellKeyToChildren should contain cellKey');
|
|
cellLists.delete(list);
|
|
|
|
if (cellLists.size === 0) {
|
|
this._cellKeyToChildren.delete(cellKey);
|
|
}
|
|
}
|
|
|
|
forEach(fn: TList => void): void {
|
|
// Fast-path for the common case of a list without nested child lists,
|
|
// which avoids allocating a Map iterator on every scroll event.
|
|
if (this._cellKeyToChildren.size === 0) {
|
|
return;
|
|
}
|
|
for (const listSet of this._cellKeyToChildren.values()) {
|
|
for (const list of listSet) {
|
|
fn(list);
|
|
}
|
|
}
|
|
}
|
|
|
|
forEachInCell(cellKey: string, fn: TList => void): void {
|
|
const listSet = this._cellKeyToChildren.get(cellKey) ?? [];
|
|
for (const list of listSet) {
|
|
fn(list);
|
|
}
|
|
}
|
|
|
|
anyInCell(cellKey: string, fn: TList => boolean): boolean {
|
|
const listSet = this._cellKeyToChildren.get(cellKey) ?? [];
|
|
for (const list of listSet) {
|
|
if (fn(list)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
size(): number {
|
|
return this._childrenToCellKey.size;
|
|
}
|
|
}
|