Двадцать элементов в ленте — это идеально. Прокрутка идет плавно, как по маслу. Ваш клиент доволен. Но затем вы выпускаете приложение в продакшн, приходят данные, и внезапно вы обнаруживаете перед собой две тысячи строк. Интерфейс начинает дергаться. Потребление памяти растет, пока операционная система не принудительно завершит процесс. В отчаянии некоторые разработчики просто оборачивают всё в ScrollView и считают, что дело сделано. Такое решение обычно порождает три новых бага на каждый исправленный.

Списки — это узкое место в производительности, которое определяет, как пользователи воспринимают ваше React Native приложение. Настройте их правильно, и приложение будет ощущаться нативным. Ошибитесь — и даже самый красивый экран превратится в мучение. Первопричина обычно кроется в несоответствии между выбранным компонентом и объемом работы, которую вы поручаете JS- и UI-потокам. React Native работает на двух путях: ваша логика живет в JS-потоке, в то время как отрисовка происходит в нативном UI-потоке. Когда вы неправильно рендерите массивный список, оба потока начинают тонуть в расчетах макета, ререндерах и выделении памяти. Результат — пропущенные кадры, пустые вспышки и, в конечном итоге, вылет приложения.

Выбирайте правильный инструмент

Выбор компонента списка должен быть осознанным архитектурным решением, а не рефлексом.

ScrollView — самый простой вариант. Он берет все переданные ему дочерние элементы, сразу монтирует каждый из них в память и передает всю эту массу нативному движку прокрутки. Это именно то, что вам нужно для короткого, фиксированного контента, такого как экран настроек, форма входа или статичная страница товара с десятью разделами. Он предсказуем и прост в стилизации. Подвох в том, что в нем нет виртуализации. Если вы скормите ему две тысячи элементов, он послушно создаст две тысячи нативных представлений. Не используйте ScrollView для больших или динамических наборов данных. Думайте о нем как о картине в раме, а не о книжной полке.

FlatList — это рабочая лошадка для длинных однотипных лент. Он виртуализирует контент, что означает, что он монтирует только те строки, которые в данный момент видны или находятся рядом с областью просмотра. По мере прокрутки FlatList размонтирует ячейки, уходящие с экрана, и переиспользует их для новых данных. Это позволяет удерживать потребление памяти на стабильном уровне, независимо от того, насколько большим становится ваш массив. Если вы создаете ленту социальных сетей, центр уведомлений или любую непрерывно прокручиваемую коллекцию похожих карточек, FlatList — это стандартный и правильный выбор.

SectionList — это FlatList с упором на структурирование. Используйте его, когда данные приходят сгруппированными, например, адресная книга, отсортированная по алфавиту, журнал тренировок, разделенный по датам, или список счетов, организованный по месяцам. Он отрисовывает липкие (sticky) заголовки секций и берет на себя логику группировки. Под капотом он использует тот же движок виртуализации, что и FlatList, поэтому вы получаете те же преимущества в плане памяти, но с дополнительной структурой в виде именованных разделов.

FlashList вступает в игру, когда вам нужно выжать из устройства максимум кадров. Построенный на базе экосистемы RecyclerListView, он переиспользует представления агрессивнее, чем FlatList, и стремится поддерживать шестьдесят кадров в секунду даже на устройствах среднего сегмента. Если вы создаете высоконагруженный чат, каталог товаров с высокой скоростью прокрутки или любой экран, где плавность является конкурентным преимуществом, FlashList стоит того, чтобы добавить его в зависимости. Он не обязателен для каждого экрана, но для лент, определяющих основной пользовательский опыт, разница в производительности будет заметна.

Основные убийцы производительности

Когда список начинает тормозить, обычно виноваты три подозреваемых.

Монтирование слишком большого количества деревьев React одновременно — это самый заметный провал. Когда каждая строка представляет собой сложное дерево компонентов, первичный рендер может заблокировать JS-поток на достаточно долгое время, чтобы вызвать появление белого экрана или неприятную задержку первой отрисовки (first paint). Пользователь открывает приложение и ждет. Даже после начальной загрузки тяжелые строки замедляют инициализацию прокрутки, так как первые несколько кадров расходуются на подготовительную работу.

Слишком много работы за один кадр проявляется в виде заиканий при прокрутке. У вас есть бюджет примерно в шестнадцать миллисекунд на кадр, чтобы анимации оставались плавными. Если компонент строки выполняет дорогостоящие вычисления, парсит даты на лету или проводит глубокое сравнение объектов внутри рендера, вы выходите за пределы этого бюджета. UI-поток пропускает кадры, и пользователь чувствует рывки.

Чрезмерное потребление памяти — это тихий убийца. Каждый нативный элемент стоит оперативной памяти. Добавьте неоптимизированные изображения большого размера, тени на каждой карточке или вложенные touchable-элементы — и объем занимаемой памяти вырастет в геометрической прогрессии. На 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 больше не знает, какие строки должны быть видимыми. В результате каждая строка всё равно монтируется, что сводит на нет саму суть виртуализации. Если вам нужен заголовок над списком, используйте собственный пропс FlatListListHeaderComponent. Если вам нужно сложное поведение «липкого» заголовка, используйте SectionList или FlashList с соответствующей конфигурацией заголовка.

Золотое правило

Если контент небольшой и конечный, пусть с ним справляется ScrollView. Если контент растет за счет пользовательских данных или удаленной пагинации, используйте виртуализированный список. Когда список является центральным элементом приложения и пользователи будут прокручивать его минутами, выбирайте FlashList.

И последняя истина, о которой легко забыть: скучные строки прокручиваются быстро. Чем легче ваш компонент строки, тем плавнее ваш список. Избавьтесь от вложенной навигации, тяжелых вычислений и избыточных анимаций внутри отдельной строки. Держите разметку плоской, логику — минималистичной, а размеры изображений — фиксированными. Жизнь списка зависит от совокупного веса того, что он отрисовывает. Сделайте каждую строку «дешевой», и список будет ощущаться «дорогим» в самом лучшем смысле этого слова.