Двадцять елементів у стрічці — це ідеально. Прокрутка ідеально плавна. Ваш клієнт задоволений. Але потім ви випускаєте продукт у продакшн, приходять дані, і раптом ви бачите дві тисячі рядків. Інтерфейс починає смикатися. Споживання пам'яті зростає, поки ОС не припинить роботу додатка. У відчаї деякі розробники загортають усе в ScrollView і вважають, що справу зроблено. Таке рішення зазвичай породжує три нові баги на кожен один, який воно виправляє.
Списки — це вузьке місце продуктивності, яке визначає, як користувачі сприймають ваш React Native додаток. Зробіть їх правильно — і додаток відчуватиметься нативним. Зробіть їх неправильно — і навіть найгарніший екран перетвориться на муку. Першопричиною зазвичай є невідповідність між обраним компонентом і обсягом роботи, яку ви покладаєте на JavaScript та UI потоки. React Native працює на двох рівнях. Ваша логіка живе в JS thread, тоді як малювання відбувається в native UI thread. Коли ви неправильно рендерите величезний список, обидва потоки починають тонути в розрахунках макета, ререндерах та виділенні пам'яті. Результатом є пропущені кадри, білі спалахи та, зрештою, виліт додатка.
Оберіть правильний інструмент
Вибір компонента списку має бути свідомим архітектурним рішенням, а не рефлексом.
ScrollView — це найпростіший варіант. Він бере кожного дочірнього елемента, якого ви йому даєте, негайно монтує кожен із них у пам'ять і передає всю цю купу нативному рушію прокрутки. Це саме те, що потрібно для короткого, фіксованого контенту, такого як екран налаштувань, форма входу або статична сторінка деталей продукту з десятьма розділами. Він передбачуваний і його легко стилізувати. Підвох полягає в тому, що в ньому немає віртуалізації. Якщо ви передасте йому дві тисячі елементів, він слухняно створить дві тисячі нативних представлень. Не використовуйте ScrollView для великих або динамічних наборів даних. Думайте про нього як про картину у рамі, а не як про книжкову полицю.
FlatList — це робоча конячка для довгих однотипних стрічок. Він віртуалізує контент, що означає, що він монтує лише ті рядки, які зараз видимі або знаходяться поруч із областю перегляду. Коли користувач гортає список, FlatList розмонтовує комірки, що виходять за межі екрана, і переробляє їх для нових даних. Це дозволяє підтримувати стабільний рівень споживання пам'яті незалежно від того, наскільки великим стає ваш масив. Якщо ви створюєте стрічку новин у соцмережі, центр сповіщень або будь-яку колекцію схожих карток, що постійно прокручується, FlatList є стандартним правильним вибором.
SectionList — це FlatList із певною організованістю. Використовуйте його, коли ваші дані приходять згрупованими, наприклад, адресна книга, відсортована за алфавітом, журнал тренувань, розділений за датами, або список рахунків, організований за місяцями. Він рендерить липкі (sticky) заголовки розділів і бере на себе логіку групування. Під капотом він використовує той самий рушій віртуалізації, що й FlatList, тому ви отримуєте такі ж переваги в плані пам'яті, але з додатковою структурою за назвами розділів.
FlashList вступає в гру, коли вам потрібно витиснути з пристрою кожен останній кадр. Побудований на базі екосистеми RecyclerListView, він переробляє представлення агресивніше, ніж FlatList, і прагне підтримувати шістдесят кадрів на секунду навіть на середньокласних пристроях. Якщо ви створюєте високопродуктивний чат, каталог товарів із швидкою прокруткою або будь-який екран, де плавність є конкурентною перевагою, FlashList вартий додавання нової залежності. Він не потрібен для кожного екрана, але для стрічок, що визначають основний досвід користувача, різниця у продуктивності буде помітною.
Поширені вбивці продуктивності
Коли список починає гальмувати, зазвичай винними є три підозрювані.
Монтування занадто великої кількості дерев React одночасно — це найбільш драматичний провал. Коли кожен рядок є складним деревом компонентів, початковий рендеринг може заблокувати JS thread настільки довго, що це призведе до білого екрана або неприємної затримки першого малювання (first paint). Користувач відкриває додаток і чекає. Навіть після початкового завантаження важкі рядки роблять ініціалізацію прокрутки повільною, оскільки перші кілька кадрів витрачаються на підготовчі роботи.
Занадто багато роботи за один кадр проявляється як смикання під час прокрутки. У вас є бюджет приблизно у шістнадцять мілісекунд на кадр, щоб підтримувати плавність анімації. Якщо компонент рядка виконує дорогі обчислення, парсить дати на льоту або проводить глибоке порівняння об'єктів всередині render, ви перевищуєте цей бюджет. UI thread пропускає кадри, і користувач відчуває ривки.
Надмірне споживання пам'яті — це тихий убивця. Кожне нативне представлення споживає RAM. Додайте великі неоптимізовані зображення, тіні на кожну картку або вкладені touchables — і обсяг споживання пам'яті помножиться. На iOS система може завершити роботу вашого додатка без попередження. На Android користувач спостерігає за наростанням затримок, поки додаток не стане зовсім непридатним для використання.
Контрольний список для оптимізації
Невеликі стратегічні звички відрізняють список, який просто працює, від того, що працює блискавично.
Використовуйте стабільні ключі. Завжди передавайте реальний ідентифікатор із вашого набору даних у проп key. Ніколи не використовуйте індекс масиву. Якщо ваш список змінює порядок, фільтрується або до нього додаються елементи, ключ на основі індексу збиває React з пантелику, змушуючи його поєднувати неправильні дані з неправильним повторно використаним компонентом. Ця помилка спричиняє непотрібні розмонтування (unmounts), невідповідність станів та каскадні рендери. Правильний ID чітко вказує React, який саме рядок куди перемістився.
Мемоїзуйте рядки. Огорніть компонент рядка в React.memo, щоб він перемальовувався лише тоді, коли його пропси дійсно змінюються. Без цього захисту будь-яке оновлення стану батьківського компонента може спричинити цикл рендерингу для кожного видимого рядка, навіть якщо їхні дані ідентичні. У довгому списку, що постійно оновлюється, ці витрачені цикли швидко накопичуються.
Зберігайте стабільність renderItem. Уникайте визначення нової функції безпосередньо всередині пропа renderItem під час кожного рендерингу батьківського компонента. Інлайнова стрілкова функція, як-от renderItem={({ item }) => <Row data={item} />}, створює нове посилання щоразу, коли оновлюється батьківський компонент. FlatList бачить змінений проп і непотрібно перевикористовує рядок. Визначте функцію рендерингу поза компонентом або мемоїзуйте її за допомогою useCallback, щоб посилання залишалося стабільним.
Використовуйте getItemLayout, коли це можливо. Якщо ваші рядки мають фіксовану або передбачувану висоту, чітко вкажіть це для FlatList. Цей проп дозволяє списку пропускати дорогі виклики нативних вимірювань. Замість того, щоб вимірювати кожну комірку після монтування, список обчислює позицію математично. Різниця стає особливо помітною у списках із сотнями або тисячами елементів, де постійні виклики onLayout можуть паралізувати JS-потік.
Агресивно оптимізуйте зображення. Необмежені зображення — це отрута для списків. Завжди встановлюйте явну ширину та висоту, щоб нативний шар резервував місце ще до декодування зображення. Для віддалених зображень використовуйте бібліотеку кешування, таку як Expo Image або аналогічну, яка забезпечує кешування в пам'яті, збереження на диску та оптимізацію форматів. Стандартний компонент React Native Image підходить для прототипів, але для реальних проєктів (production) потрібен більший контроль над пам'яттю та станами завантаження.
Уникайте вкладення контейнерів прокрутки. Ніколи не розміщуйте вертикальний FlatList всередині вертикального ScrollView. Батьківський ScrollView перехоплює всі події прокрутки та заважає дочірньому FlatList вимірювати свою область перегляду (viewport). Віртуалізація порушується, оскільки FlatList більше не знає, які рядки мають бути видимими. У результаті кожен рядок все одно монтується, що нівелює саму суть віртуалізації. Якщо вам потрібен заголовок над списком, використовуйте власний проп FlatList — ListHeaderComponent. Якщо вам потрібна складна поведінка «липкого» заголовка (sticky), використовуйте SectionList або FlashList з відповідною конфігурацією заголовка.
Золоте правило
Якщо контент невеликий і обмежений, дозвольте ScrollView обробити його. Якщо ж контент зростає завдяки даним користувачів або віддаленій пагінації, використовуйте віртуалізований список. Коли список є центральним елементом додатка і користувачі будуть гортати його хвилинами, обирайте FlashList.
І останню істину легко забути. Прості рядки прокручуються швидко. Чим легший ваш компонент рядка, тим плавніший ваш список. Приберіть вкладену навігацію, важкі обчислення та зайві анімації з окремого рядка. Тримайте розмітку пласкою, логіку — легкою, а розміри зображень — визначеними. Життя списку залежить від сумарної ваги того, що він рендерить. Зробіть кожен рядок «дешевим», і список відчуватиметься «дорогим» у найкращому сенсі цього слова.
