Лента, каталог, чат, история заказов: почти в каждом приложении есть длинный список, и это самое частое место, где RN-приложение тормозит. Поэтому вопрос «как оптимизировать FlatList» звучит на каждом втором собесе.
Почему не ScrollView
ScrollView рендерит всех детей сразу. Тысяча карточек значит тысяча нативных представлений в памяти и долгий первый рендер. Списки в RN виртуализированы: в дереве только видимые элементы и запас вокруг, остальные заменены пустым местом.
FlatList
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem}
onEndReached={loadMore}
onEndReachedThreshold={0.5}
refreshing={isRefreshing}
onRefresh={refresh}
ListEmptyComponent={<Empty />}
/>
keyExtractorдаёт стабильный ключ. Индекс в ключе ломает состояние элементов при вставке в начало, как и в вебе.onEndReachedвызывается, когда до конца осталось меньшеonEndReachedThresholdвидимой высоты (0.5 это половина экрана). Может вызваться несколько раз подряд, поэтому нужна защита от повторной загрузки.SectionListдля групп с заголовками,horizontalдля горизонтальных лент,numColumnsдля сетки.
Настройки виртуализации
| Проп | Что делает |
|---|---|
initialNumToRender | сколько элементов отрисовать сразу (по умолчанию 10) |
windowSize | размер окна отрисовки в экранах (по умолчанию 21: 10 сверху, 10 снизу и видимый) |
maxToRenderPerBatch | сколько элементов дорисовывать за один проход |
removeClippedSubviews | отсоединять невидимые нативные представления |
getItemLayout | заранее известная высота: список не меряет элементы и умеет прыгать к индексу |
Уменьшение windowSize экономит память, но при быстрой прокрутке появляются пустые места. Это компромисс, а не волшебная настройка.
getItemLayout
Если высота элементов одинакова, список может вычислить положение любого элемента без рендера:
const ITEM = 72;
const getItemLayout = (_: unknown, index: number) => ({
length: ITEM,
offset: ITEM * index,
index,
});
Это ускоряет прокрутку и делает надёжным scrollToIndex. Если высота разная, неверный getItemLayout хуже его отсутствия: элементы будут прыгать.
Правила быстрого элемента
- Мемоизированный элемент.
const Row = memo(function Row({ item, onPress }) {...}). Без этого каждое обновление списка перерисует все видимые строки. - Стабильные колбэки.
renderItemи обработчики черезuseCallback. Стрелка, создаваемая в каждом рендере, ломаетmemo. - Лёгкая разметка. Меньше вложенных
View, без тяжёлых вычислений в рендере. - Картинки нужного размера и с кешем (
expo-imageилиreact-native-fast-image), а не фото по 4 МБ. extraData.FlatListэтоPureComponent: если элементы зависят от состояния внеdata(например, выбранный id), передайте его вextraData, иначе список не перерисуется.
FlashList
@shopify/flash-list не создаёт новые компоненты при прокрутке, а переиспользует (recycling) уже созданные, подставляя новые данные, как RecyclerView на Android и UICollectionView на iOS. Отсюда меньше работы для JS и меньше пустых мест при быстрой прокрутке. Следствие переиспользования: локальное состояние внутри элемента (useState) может «переехать» к другим данным, поэтому его сбрасывают по смене id или хранят снаружи. Вторая версия FlashList работает только на новой архитектуре и сама измеряет элементы.
Частые ошибки
ScrollViewсmapпо сотням элементов.- Ключ по индексу.
- Инлайновый
renderItemи не мемоизированные строки. onEndReachedбез флага загрузки: дубли страниц.- Список внутри
ScrollViewтой же ориентации: виртуализация ломается, RN предупреждает об этом.
Что спросят на собесе
- Как работает виртуализация и чем
FlatListлучшеScrollView. - Как оптимизировать медленный список: пропсы и мемоизация.
- Зачем
getItemLayoutиextraData. - Как сделать бесконечную подгрузку без дублей.
- Чем FlashList отличается от FlatList и какой подвох у переиспользования.