Лишние рендеры, изображения и память
Вопрос уровня middle+: как найти причину тормозов и почему приложение падает без единой ошибки в логах.
Пропущенный кадр — это кадр, который не успел отрисоваться за отведённые ~16 мс (при 60 FPS). Пользователь воспринимает такие пропуски как «дёрганый» интерфейс.
Вопрос
«Экран подтормаживает, а на бюджетном Android приложение иногда закрывается само. Как искать причину?»
Что на самом деле проверяет интервьюер
Метод. Есть разница между «я добавляю useMemo, пока не станет быстрее» и «я снимаю профиль, вижу, что 80% времени уходит на рендер одного компонента, и работаю с ним».
Развёрнутый ответ
Два разных «тормозит»
- JS-поток перегружен: лишние рендеры, тяжёлые вычисления, парсинг больших ответов. Симптом — отклик на нажатия запаздывает, анимации на JS дёргаются.
- UI-поток перегружен: слишком сложная иерархия view, дорогая отрисовка, тени и прозрачности, огромные картинки. Симптом — дёргается сам скролл, даже когда JS свободен.
Разделять их важно: лечение разное. В dev-меню есть Perf Monitor, который показывает FPS обоих потоков по отдельности — с него и стоит начинать.
Поиск лишних рендеров
React DevTools Profiler записывает коммиты и показывает, какие компоненты обновились, сколько заняли и почему. Быстрая ручная проверка тоже годится:
function Row({ item }) {
if (__DEV__) console.log('render Row', item.id);
// ...
}
Частые причины лавины рендеров: нестабильное значение в Context, состояние, поднятое слишком высоко, селекторы, возвращающие новые объекты, и renderItem, объявленный прямо в JSX.
Анимации: не занимать JS-поток
// нативный драйвер: JS не участвует в отрисовке кадров
Animated.spring(translateY, { toValue: 0, useNativeDriver: true }).start();
С useNativeDriver работают трансформации и прозрачность, но не свойства, влияющие на layout (width, height, top). Для жестов и сложных анимаций берут Reanimated: его worklet-функции выполняются прямо на UI-потоке, поэтому анимация не зависит от загруженности JS.
Изображения — главный пожиратель памяти
Декодированная картинка занимает примерно ширина × высота × 4 байта. Фотография 4000 на 3000 — это около 48 МБ в памяти, независимо от того, что на экране она размером с аватарку. Десять таких в списке — и Android убивает процесс, а на iOS приходит предупреждение о нехватке памяти.
- Запрашивайте с сервера уже уменьшенные версии (thumbnails), а не оригиналы.
- Используйте библиотеку с кэшем и прогрессивной загрузкой:
expo-imageилиreact-native-fast-image. - Не держите base64-строки в состоянии — это и память, и нагрузка на сериализацию.
- Чистите кэш картинок при предупреждении о нехватке памяти.
Другие источники утечек
useEffect(() => {
const id = setInterval(poll, 5000);
const sub = eventEmitter.addListener('update', onUpdate);
return () => {
clearInterval(id); // без очистки таймер живёт после ухода с экрана
sub.remove();
};
}, [poll, onUpdate]);
Классика: незакрытые таймеры и подписки, слушатели навигации без remove, растущие массивы в глобальном store, кэш без ограничения размера.
Про размер бандла и старт
Холодный старт — тоже метрика производительности, и о ней спрашивают. Помогают Hermes (байт-код заранее), ленивые импорты тяжёлых экранов, отказ от «библиотеки ради одной функции», отложенная инициализация аналитики и SDK после первого кадра.
Типичные ошибки кандидатов
- Начинают с расстановки
useMemoвместо измерения. - Не разделяют JS-поток и UI-поток, поэтому лечат не ту проблему.
- Считают, что маленький размер файла картинки означает малое потребление памяти — важен размер в пикселях после декодирования.
- Не помнят про ограничения
useNativeDriverи обещают анимировать имheight.
Как ответить кратко
«Сначала измеряю: Perf Monitor показывает, какой поток просел — JS или UI. Лишние рендеры ищу профайлером React DevTools; типичные причины — нестабильное значение Context, слишком высоко поднятое состояние, селекторы с новыми объектами и renderItem в JSX. Анимации увожу с JS-потока: useNativeDriver для трансформаций и прозрачности, Reanimated для жестов. Падения по памяти почти всегда из-за картинок: декодированное изображение занимает ширину на высоту на четыре байта, поэтому беру уменьшенные версии с сервера и библиотеку с кэшем. Плюс слежу за очисткой таймеров и подписок и за временем холодного старта.»