Расскажите о сложной проблеме, которую вы решали в React Native проекте.
Одна из наиболее сложных проблем, с которой пришлось столкнуться в React Native проекте, была связана с интеграцией высоконагруженного видеофида (аналогично TikTok/Instagram Reels) с автопроигрыванием, бесконечной прокруткой, предзагрузкой видео, управлением звуком, UI-анимациями и кэшированием. Это был основной экран приложения, и от его плавности и отзывчивости напрямую зависело пользовательское восприятие качества продукта.
Основные цели и сложности
-
Отображение вертикального фида с видеороликами.
-
Автоматическое воспроизведение текущего видео, остановка предыдущего.
-
Плавный переход между видео с анимацией.
-
Предзагрузка соседних видео (вверх и вниз).
-
Кеширование видеофайлов, чтобы не нагружать сеть.
-
Уменьшение потребления памяти и тормозов на Android.
Сложность заключалась в том, что даже в ранней версии экрана:
-
UI начинал лагать уже при прокрутке 3–4 видео;
-
Звук иногда начинался с задержкой;
-
Видео не воспроизводилось плавно;
-
Список часто ререндерился при любом изменении.
Анализ и инструменты
1. Анализ производительности
-
Использовался Flipper + Perf Monitor для анализа JS Thread.
-
why-did-you-render показывал лишние ререндеры.
-
react-native-performance помог отслеживать потерю кадров на Android.
2. Профилирование прокрутки
-
FlatList с initialNumToRender: 3, но не настроены windowSize и getItemLayout.
-
Были видны скачки FPS при первом запуске видео и при переходе между элементами.
3. Проблемы с видео
-
Использовалась библиотека react-native-video, но с ошибками:
-
Видео загружалось каждый раз при маунте.
-
При быстром скролле проигрывались сразу несколько видео.
-
Проблемы с управлением состоянием (pause/play) при переходе между карточками.
-
Подход к решению
1. Управление видео через ref и index
Создан глобальный провайдер, отслеживающий активный индекс. Каждый компонент VideoCard получает свой index, сравнивает с активным и запускает/останавливает видео:
useEffect(() => {
if (currentIndex === index) {
videoRef.current?.playAsync();
} else {
videoRef.current?.pauseAsync();
}
}, \[currentIndex\]);
2. FlatList оптимизация
-
windowSize: 5
-
maxToRenderPerBatch: 2
-
removeClippedSubviews: true
-
getItemLayout: так как все карточки были одинаковой высоты
Это снизило нагрузку на память и сократило число рендеров.
3. Кеширование видео
Перешли на react-native-video-cache + react-native-fs. Видео загружались и сохранялись локально, проверялись по URL. При повторном открытии они подгружались из кеша. Это убрало зависание при первом старте.
4. Отложенная загрузка и prefetch
Добавлено предзапрос видео по индексу current + 1 и current - 1 с помощью:
useEffect(() => {
prefetchVideo(videoList\[currentIndex + 1\]?.url);
prefetchVideo(videoList\[currentIndex - 1\]?.url);
}, \[currentIndex\]);
5. Ограничение на количество одновременно активных видео
Ввелся центральный контроллер через контекст, который позволял проигрываться только одному видео. Остальные получали команду на pause.
6. Анимации с Reanimated 2
Изначально анимации перелистывания были через Animated, что загружало JS Thread. Перешли на Reanimated 2 с использованием Worklets. Пример:
useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});
Вся логика прокрутки и определения активного индекса была вынесена в UI Thread.
7. Обработка ошибок
Добавлена fallback-логика:
-
При ошибке загрузки — показывался заглушечный кадр.
-
Логгировались ошибки в Sentry.
-
Устанавливались таймауты на загрузку и паузы между действиями.
Результаты
-
Прокрутка стала плавной даже на Android mid-range (4 ГБ RAM).
-
Видео проигрывались корректно без запинок.
-
Количество одновременно активных плееров — 1.
-
Память под контролем: мониторинг показал снижение использования на 40–60 МБ.
-
Были покрыты юнит-тестами логика проигрывания и кэширования.
-
Поведение стало консистентным на Android и iOS.
Эта задача потребовала комплексного подхода: оптимизации UI, использования Reanimated, управления стейтом, работы с кешем и нативными возможностями. Основная сложность заключалась в том, что проблему нельзя было решить одной библиотекой или хуком — требовалась связка инженерных решений на нескольких уровнях.