Keep-Alive Pattern for Tab Navigation
Summary
Архитектурное решение для предотвращения unmount/remount компонентов при переключении вкладок в React SPA.
Проблема: При переключении вкладок компоненты unmount → изображения выгружаются из памяти → при возврате появляется мерцание загрузки.
Решение: Keep-Alive pattern - все экраны остаются в DOM, переключаются через CSS display.
Business Context
Проблема пользователя
После долгого простоя приложения (5-10+ минут) при переключении между вкладками Home и Profile:
- Изображение Steam логотипа "появляется внезапно" (мерцание)
- Изображение Daily Case "появляется внезапно" (мерцание)
- Другие изображения (scrap/xp иконки) не мерцают
Root Cause
-
Условный рендеринг через
{currentScreen}:// Было
<main>{currentScreen}</main>- При
activeTab=Home→ рендерится только HomeScreen - При переключении на Profile → HomeScreen unmount, ProfileScreen mount
- При возврате на Home → ProfileScreen unmount, HomeScreen mount заново
- При
-
Browser Memory Management:
- При unmount компонента → DOM удаляется
- После долгого простоя браузер выгружает изображения из memory cache
- При mount заново → браузер запрашивает изображения с диска/сети
- Пользователь видит момент загрузки (fade-in, мерцание)
-
Почему только Steam и Case:
- Находятся в разных unmount-able экранах (Profile, Home)
- Scrap/XP иконки всегда в DOM (BalanceSection не unmount)
Technical Solution
Implementation
До (условный рендеринг):
const currentScreen = useMemo(() => {
switch (activeTab) {
case Tab.Home: return homeScreen;
case Tab.Profile: return profileScreen;
// ...
}
}, [activeTab, ...]);
return <main>{currentScreen}</main>; // ← UNMOUNT при переключении
После (Keep-Alive):
// Убрали currentScreen useMemo
// Рендерим все экраны, переключаем через display
return (
<main>
<div style={{ display: activeTab === Tab.Home ? 'block' : 'none' }}>
{homeScreen}
</div>
<div style={{ display: activeTab === Tab.Profile ? 'block' : 'none' }}>
{profileScreen}
</div>
<div style={{ display: activeTab === Tab.Cases ? 'block' : 'none' }}>
{casesScreen}
</div>
<div style={{ display: activeTab === Tab.Spin ? 'block' : 'none' }}>
{dailySpinScreen}
</div>
</main>
);
Преимущества
| Аспект | До | После |
|---|---|---|
| Unmount | Да, при переключении | Нет, остаются в DOM |
| Изображения | Выгружаются из памяти | Остаются в кэше |
| Состояние | Теряется (scroll, forms) | Сохраняется |
| Переключение | Re-render | Только CSS change |
| Мерцание | Да, после простоя | Нет |
Performance Impact
Memory:
- 4 экрана в DOM вместо 1 (+3 скрытых)
- Каждый экран: ~50-100 DOM nodes
- Total overhead: ~200-300 nodes (минимально для современных браузеров)
CPU:
- Нет лишних unmount/mount операций
- Нет re-render при переключении
- Только CSS
displaychange (быстро)
Network:
- React Query с
staleTimeпредотвращает лишние refetch для скрытых экранов - Изображения загружаются 1 раз и остаются в памяти
ADR (Architectural Decision Record)
Контекст
React SPA с bottom navigation и 4 вкладками (Home, Profile, Cases, Spin).
Пользователи часто переключаются между вкладками и держат приложение открытым долго (15+ минут).
Решение
Выбран: Keep-Alive Pattern (все экраны в DOM, переключение через display)
Альтернативы (отклонены):
-
Priority prop для LazyImage ❌
- Костыль, не решает root cause (unmount)
- Не работает для простых
<img>тегов
-
CSS Preload Hint ❌
- Не решает unmount (могут быть другие проблемы)
- Требует ручного управления списком изображений
-
React.lazy с Suspense ❌
- Усугубляет проблему (ещё больше unmount)
- Добавляет loading states
Последствия
Положительные:
- ✅ Изображения больше не мерцают
- ✅ Состояние компонентов сохраняется
- ✅ Мгновенное переключение вкладок
- ✅ Стандартный React паттерн (используется в Telegram Web, Discord, Slack)
Нейтральные:
- 🔸 Все компоненты в DOM (но overhead минимальный)
- 🔸 Нужно следить чтобы скрытые экраны не выполняли лишних операций
Отрицательные:
- Нет значимых
Usage Guidelines
Когда использовать Keep-Alive
✅ Используй для:
- SPA с bottom/top navigation и несколькими вкладками (2-10)
- Экраны с дорогими операциями (сложные формы, charts)
- Экраны с изображениями/медиа контентом
- Экраны где важно сохранить состояние (scroll, filters)
❌ Не используй для:
- Модальных окон (используй conditional rendering)
- Deep navigation (используй React Router)
- Большого количества экранов (>10, memory pressure)
Best Practices
-
Оптимизация скрытых экранов:
useEffect(() => {
if (activeTab !== Tab.Home) return; // Пропустить если скрыт
// Выполнить операцию только для активной вкладки
}, [activeTab]); -
React Query staleTime:
useQuery('key', fetcher, {
staleTime: 5 * 60 * 1000, // 5 минут - не refetch при unmount
}); -
Мемоизация экранов:
const homeScreen = useMemo(() => <HomeScreen />, []);
// Предотвращает лишние re-renders
Related
- Image Preloader - предзагрузка критичных изображений
- React Query Setup - оптимизация refetch
- Performance Monitoring - отслеживание performance impact