Компроміси React Native у масштабі: стан, списки, пристрої, токени, оновлення
П’ять проблем React Native, які виявляють інженерне мислення: стан сервера та клієнта, затримки у FlatLists, пристрої Android низької потужності, зберігання токенів та поетапні оновлення.
Створення інтерфейсу в React Native є простим. Складнощі з’являються, коли додаток росте: стан поширюється всюди, списки працюють з перешкодами, смартфони на Android падають, токени зберігаються у відкритому форматі, а фреймворк відстає на кілька років. Нижче наведено п’ять таких ситуацій, які часто використовуються для перевірки досвідчених інженерів під час співбесід, разом із аргументами, які має містити хороша відповідь, щоб ви могли застосувати їх до власного кодового базису.
Розділення серверного стану від клієнтського стану
Коли локальний стан, відповіді API, кеш та спільні прапорці інтерфейсу перетворюються на єдиний клубок, перше запитання полягає не у „Redux чи Context?“, а у „хто є власником цих даних?“
Дані з API, такі як профілі, ленти новин чи списки продуктів, є станом сервера. Вони стають застарілими, тому їх необхідно знову отримувати, кешувати та анулювати їхню дійсність. Зберігання їх у глобальному сховищі означає необхідність вручну повторно реалізувати всі ці процеси, а також додати механізми завантаження та відстеження помилок. Наведений нижче фрагмент демонструє саме цей антипаттерн.
// ❌ Server data forced into a global store —
// now YOU own caching, staleness, invalidation, loading states…
dispatch(setProducts(await fetchProducts()));
TanStack Query та RTK Query створені для керування цим рівнем. Тут функція useQuery групує дані за категоріями та підтримує їх актуальність протягом 60 секунд завдяки параметру staleTime. Друга частина блоку (об’єднана в одну у вихідному коді) показує, що залишається для глобального сховища: невеликий об’єкт сеансу.
// ✅ Server state managed by a tool built for it
const { data, refetch } = useQuery({
queryKey: ['products', category],
queryFn: () => fetchProducts(category),
staleTime: 60_000,
});// ✅ Truly shared client state — small and intentional
const useSession = create<SessionStore>((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
Решта проблеми значно менша. Полі форм, перемикачі та значення анімацій залишаються в компоненті, де вони не потребують багато ресурсів, ізольовані та зникають разом із екраном. Глобальний сховище має містити лише ті дані, які потрібні кільком екранам та належать самому клієнту, наприклад автентифікований користувач, тема, флаги функцій чи стан інтерфейсу, який поширюється на кілька екранів.
Що ламається, коли все є глобальним
Оновлення змушують переробляти непов’язані екрани, кожна функція прив’язується до структури сховища, тести мусять імітувати реальний світ, а рефакторинг перетворюється на складну роботу. Надмірно розросле сховище виглядає як архітектура, але поводиться як борг.
Діагностика повільного FlatList без здогадок
FlatList виглядає повільним, хоча API працює швидко. Використання React.memo скрізь — це здогадки; правильний підхід — спочатку виміряти.
Профіль із React DevTools (або бібліотекою why-did-you-render) під час прокрутки. Якщо кожна рядок перерендерується при кожному кроці прокрутки, причиною є ідентичність посилань: вбудовані функції-стрілки у renderItem, об’єкти стилів, які перебудовуються з кожним рендером, або відсутність keyExtractor, що змушує до повторного монтування. Тут кожен рендер створює новий об’єкт onPress та style, тому жодна рядок не може пропустити рендерування.
// ❌ New function + new object on EVERY render → every row re-renders
<FlatList
data={items}
renderItem={({ item }) => (
<Row item={item} onPress={() => open(item.id)} style={{ padding: 12 }} />
)}
/>
Рішення полягає у забезпеченні React стабільних посилань: мемоїзований Row, renderItem, обгорнутий у useCallback, стабільний ключ та getItemLayout, щоб список ніколи не вимірював рядки. Типовані пропси роблять цей код TSX.
// ✅ Stable identities + memoized rows
const Row = React.memo(({ item, onPress }: RowProps) => { /* … */ });const renderItem = useCallback(
({ item }: ListRenderItemInfo<Item>) => <Row item={item} onPress={handlePress} />,
[handlePress],
);<FlatList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
getItemLayout={(_, index) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index,
})}
/>
getItemLayout підходить лише для рядків з фіксованою висотою; неправильні значення для рядків з змінною висотою спричиняють стрибки та порожні проміжки.
Читання монітора продуктивності
Якщо зображення відображаються коректно, але кадри все одно зникають, порівняйте два показники Perf Monitor:
- Низький рівень JS FPS означає, що потік JavaScript перевантажений, зазвичай через виконання важких операцій у функції
renderItemабо без обмежень у функціїonScroll. - Низький рівень UI FPS вказує на проблеми з нативним кодом, зазвичай пов’язані з зображеннями. Декодування фото у повній роздільній здатності для створення вміщувальних зображень розміром 80 пікселів спричиняє витрату пам’яті та зниження кількості кадрів; краще змінювати розміри на серверному рівні або використовувати
expo-imageчиFastImageіз правильними параметрами.
Лише після цього можна налаштовувати параметри windowSize, removeClippedSubviews або перейти на використання FlashList. Налаштування параметрів списку до аналізу даних призводить лише до усунення симптомів.
Виявлення збоїв на пристроях Android з обмеженими можливостями
Додаток, який працює плавно на флагманських телефонах, може зупинитися на дешевих Android-пристроях, які мають більшість користувачів. Почніть з даних: Play Console чи інструменти аналітики показують реальний склад пристроїв, який рідко збігається з телефонами у вашій команді.
Зробіть обладнання низького класу частиною щоденної роботи: тримайте поруч фізичний пристрій з 2–3 ГБ оперативної пам’яті або використовуйте Firebase Test Lab для тестування на моделях, вказаних у вашій аналітиці, як це робить ця команда.
gcloud firebase test android run \
--app app-release.apk \
--device model=a10,version=29 \
--device model=redmi9,version=30 # the phones in your analytics, not yours
Завжди тестуйте версії для релізу. Версії для дебаггінгу приховують справжню продуктивність, а Hermes у режимі релізу поводиться інакше, ніж під час дебаггінгу. Типові проблеми на слабкому обладнанні — це нестача пам’яті через великі зображення чи списки, перевантажена основна потокова операція та зупинка програми через нестачу пам’яті, що ніколи не трапляється на флагманських телефонах.
Плавне погіршення функціоналу замість зупинки
Ви можете налаштувати функціонал відповідно до кожного пристрою. react-native-device-info повідомляє про загальну кількість пам’яті.
import DeviceInfo from 'react-native-device-info';
За допомогою цього інструменту можна позначити пристрої з менш ніж 3 ГБ як низькокласні, використовувати мініатюри замість повних зображень та пропускати ефекти розмиття, паралаксу та важкі анімації. Оскільки getTotalMemory є асинхронним, обчислюйте позначку один раз під час запуску, а не в очікуванні під час відображення.
const totalMemory = await DeviceInfo.getTotalMemory();
const isLowEnd = totalMemory < 3 * 1024 ** 3; // < 3 GB RAM<Image
source={{ uri: isLowEnd ? item.thumbUrl : item.fullResUrl }}
// skip blur, parallax and heavy animations on low-end devices
/>
Потім розробляйте з захистом: діліть звіти Sentry чи Crashlytics за класом пристроїв, використовуйте поетапну розгортку у Play Store (5%, 20%, 100%) та зупиняйтесь, перш ніж несправна версія потрапить до всіх користувачів. Мета — не повна відсутність збоїв, а їхнє раннє та економічне виявлення.
Безпечне зберігання токенів автентифікації
AsyncStorage зберігає дані без шифрування на диску: файл SQLite в Android, звичайні файли sandbox у iOS. Пристрій із рутованим чи jailbroken системою, зловмисний бекап або доступ до файлової системи можуть безпосередньо викрити токени. Він створений для зберігання налаштувань, а не секретних даних.
Токени мають зберігатися у сховищі, підтримуваному апаратним забезпеченням — Keychain у iOS та Keystore у Android, які інтегруються за допомогою react-native-keychain.
import * as Keychain from 'react-native-keychain';
Цей виклик зберігає серіалізовані токени за параметром WHEN_UNLOCKED_THIS_DEVICE_ONLY: їх можна читати лише тоді, коли пристрій розблокований, і вони ніколи не мігрують на інший пристрій.
await Keychain.setGenericPassword('auth', JSON.stringify(tokens), {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
Процес оновлення, який працює навіть при одночасних помилках 401
Встановіть термін дії токенів доступу у кілька хвилин, а не днів, підтримайте їх токеном оновлення, який замінюється щоразу під час обміну, та централізуйте цей процес у одному шарі автентифікації, який оновлюється рівно один раз, незалежно від кількості збоїв у запитах. Це стає можливим завдяки спільному обіцянку.
let refreshing: Promise<string> | null = null;
Інтерцептор Axios перекидає будь-яку помилку, крім 401. Для коду 401 оператор ??= запускає оновлення лише тоді, коли жодне не виконується, тож одночасні збої очікують на одне й те саме обіцяння; блок finally скидає його, і початковий запит намагається виконати знову за допомогою нового токена доступу. У вихідному коді кілька операторів розміщені на одній лінії. Для більш детального розгляду дивіться чому одночасні помилки 401 виводять користувачів та як це виправити.
api.interceptors.response.use(undefined, async (error) => {
if (error.response?.status !== 401) throw error; // Concurrent 401s all await the SAME refresh — no refresh storm
refreshing ??= refreshTokens().finally(() => (refreshing = null));
const newToken = await refreshing; error.config.headers.Authorization = `Bearer ${newToken}`;
return api.request(error.config); // retry the original request
});
Ще два моменти: не зберігайте токени у глобальному стані, доступному для JS, довше, ніж це необхідно; встановіть механізм certificate pinning для надзвичайно конфіденційних API, а також ніколи не записуйте токени, оскільки інструменти для виявлення збоїв легко фіксують заголовки запитів. Інструмент під назвою „Use SecureStore“ — це лише один з варіантів; надійна відповідь має охоплювати всі аспекти терміну служби, оновлення та ситуацій збоїв.
Оновлення дворічного додатку React Native
Додаток відстає на два роки, перерв у роботі допускатися не може, і бюджету на повну переробку немає.
Ніколи не переходьте безпосередньо до найновішої версії. Інструмент React Native Upgrade Helper показує точні відмінності між версіями; робіть одне або два незначні оновлення за крок, щоб додаток завжди залишався придатним до збірки та розгортання. Кожне таке оновлення є звичайним випуском, і саме так можна досягти повної відсутності перерв у роботі.
Спочатку проведіть аудит залежностей
Оновлення зазнають невдачі через старі, не підтримувані нативні бібліотеки, а не через сам React Native. Ще до першого кроку визначте залежності, які блокують нову архітектуру чи сучасніші вимоги до Gradle та Xcode, і замініть або створіть копію тих, що покинуті.
Автоматизуйте перевірку кожного кроку
Перед початком налаштуйте тести кінцевої точки для критичних процесів, щоб кожен крок перевірявся протягом кількох хвилин, а не шляхом ручної перевірки QA. Цей процес Maestro входить підключенням за електронною поштою з змінної середовища та перевіряє доступність сторінок домашньої сторінки, кошика та процесу оплати.
# smoke-test.yaml — run with Maestro on every upgrade hop
appId: com.myapp
---
- launchApp
- tapOn: "Log in"
- inputText: ${EMAIL}
- tapOn: "Continue"
- assertVisible: "Home"
- tapOn: "Cart"
- assertVisible: "Checkout"
Представте цю роботу керівництву як спосіб зменшення ризиків, а не як рефакторинг: кожна затримка у версію підвищує вартість наступного примусового оновлення, спричиненого правилами магазину, виходом ОС з підтримки та патчами безпеки. Невеликі кроки перетворюють страшний проект на серію нудних випусків, а саме це є метою.
Основні висновки
- Вирішіть, хто володіє кожною частиною даних; нехай бібліотека запитів керує станом сервера та зберігає глобальний сховище компактним.
- Проведіть аналіз перед оптимізацією: проблеми з ідентифікацією, робота в потоках JS та декодування зображень вимагають різних рішень.
- Протестуйте версії продукту на справжніх пристроях ваших користувачів та впроваджуйте їх поетапно.
- Ставіться до токенів як до окремої системи: забезпечте їх безпечне зберігання, короткий термін дії, їх ротацію та оновлення після одноразового використання.
- Впроваджуйте оновлення невеликими, готовими до використання кроками за підтримки автоматизованих тестів.