Рэакцыя Native: праблемы на великіх масштабах — стан, спісы, прыстроі, токены, апдэйты
Пяць проблемаў React Native, які адкрываюць праблемы інжынерскага суджэння: стан сервера проты стану кліента, спазматычна робота FlatLists, прастыя версіі Android, зберагчык токенаў і поступовыя апдэйты.
Стварэнне экрана ў React Native — гэта проста. Складнасць з’яўляецца, калі дапрацоўка расте: стан распространяецца па всім, спісы працюють з перашкодамі, телефоны на Android з низкіми характэрыстыкамі завершаюць роботу, токены зберагаюцца у простам текстовым формате, а сама платформа застаецца на колькі гадоў адстаёмай ад сучасных стандартоў. Ёсць пяць такіх ситуацый, якія часта викорыстоўваюцца ў інтэрв’ю для пераканання кандыдаў з высокым досвідам, а таксама паказана, што мусі ўключацца ў адпаведныя адказы, каб можна было застосаваць іх у сэрве свайкого коду.
Раз’едначэнне стану сервера і кліента
Калі локальны стан, адпаведзі AI-сервэсаў, кеш і спяльныя флагі UI з’еднаюцца ў адну заплутаную структуру, першы вопыт не ў тым, „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 }),
}));
Зялёны рэшчынкі ўжо значна меньшыя. Полеўы формы, пераключальнікі і значэння анімацый застаюцца ў самай складовай, дзе яны не куплююць ресурсоў, існуюць адзіно і знікаюць разам з экранам. Глобальны хранар должен хаваць толькі тыя данні, якія патрэбны калькам і якія належаць самам кліенту, напрыклад, абмовленага пользователя, тэму, флажкі функцыйнасці чыста стан UI, який дзейсніцься на калькам.
Што ламаецца, калі всё глобальнае
Апдэты перыскаляюць некалякія экраны, кожная функцыйнасць зв’язана з структурой хранара, тэсты вынужданы імітаваць реальны стан, а переработкі прыменяюцца як спосаб рашэння проблем. Занадта вялікі хранар выглядае як архітектура, але па сваім дзейсненні ўпамінае борг.
Дыагназаванне медленнага 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-тэлефонах, якія маюць большасць корыстувачаў. Пачніце з дадзеных: Play Console або аналітыка паказваюць рэальны склад прыстроек, які рэдка калі падходзіць да тэлефонаў вашай каманды.
Зробіце прыстройства нізкага класу частюя дзейнасці ў кожны дзень: трывожыце фізычны прыстрой з 2–3 ГБ RAM поблізу, або выканаўце 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. Устройства з рутам або jailbreak, зловучыя копіі даных або доступ да файлавой системы практычна адкрываюць токены. Ён створаны для прыямоў, а не для секрэтных даных.
Токены павінны зберагацца ў храненні, падтрымванамым апаратным забезпечэнням, тады як iOS Keychain і Android Keystore, якія выкарыстоўваецца пакетам 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, дыльней, чым трэба; неабходна адключыць піннінг сертыфікатаў для апытак, якія ўскладненыя з точку зору безпекі, а таксама ніколы не логаваць токены, адтакуе што інструменты для аналізу крашаў лёгка захопляюць загаловкі запытоў. У інструменте пад назвай „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 і декодаванне зображэнняяў выклікаюць разныя рашэнні.
- Тэставайце версіі, якія будуць выпускацца, на рэальных прыстроях вашых выкарыстоўначаў і вводзіце их у эксплуатацыю паэтапна.
- Спрыяйце токенам як системе: абезпечаная хранэнне, короткая трымкі, ротацыя і аднакратнае апавежанне.
- Адчыніце апгрэйды па маленькіх, гатовых да выпуску кроках, падтрымваныя аўтоматызаванымі тэстамі.