Нестабільныя залежнасці useEffect: дыагназа выкарыстоўвання акумулятара ў React Native
Пазірце, як залежна вяласць аб’екта і нехватка масів залежнасцей спрычынілі выкаранае заряджанне акумулятара і перыядкі карты ў React Native, як яго аналізаваць, і тры способы выправлення, якія запрацавалі.
Экран у рэальнага часу, які выглядае ідеяльна ў сімуляцыі, можа прыбыць з бягам, які нагревае тэлфоны і спрычынае перерывы ў анімацыях, пры чым не фіксуецца жадныя адхылення. Звычайным вінаватцем є useEffect, чыя залежнасці зменяюцца значна чащэ, чым даны, якія яго цікавяць. У гэтым аналізе рассказваецца пра такі бяг на экране для стежэння за доставкай: чаму існуе useEffect, калі ён яўляецца правым інструментам, як два незначныя помылкі ў залежнасцях прыводзяць да патлання рэндара, як проблему было выявлена за дапамогою інструментаў аналізу як у JavaScript, так і ў натыўнай частцы коду, а таксама якія тры змены яе вылечылі.
Чаму існуе useEffect
Даўжыну React 16.8 класовыя компаненты распрадзелі такія побачны эфекты, як вызовы API, падпісвы і таймеры, між трыма разнымі методамі жыцёвага циклу: componentDidMount, componentDidUpdate і componentWillUnmount. Напрыклад, логічная задача на кшталт „слухаць гэты сокет паколькі экран адображаецца“ зазвычай дзелілася між усіма трымі, што прыводзіла да рассеяння скарыстай коду і лёгкага забывання якога-небудзь крока.
У React 16.8 з’явіліся хукі, і useEffect быў створаны для аб’еднання такой логікі. Уместа таго, каб думаць пра стадіі жыцёвага циклу, вы описваеце, як компанент залишаецца сінхронным з зовнішняй системай, незалежна ад таго, чы то запит, відпаведны слухальнік, падпіс, таймер чы кадр анімацыі. Эфект выкананы пасля рэндараўвання, можа вярнуць функцыю для чысткі і зноў выканаецца кожны раз, калі змянюецца якое-небудзь значэнне ў його масе залежнасцей:
useEffect(() => {
// side effect code
return () => {
// cleanup code
};
}, [dependencies]);
У React Native гэты хук выклікаецца пастоянна, адказваючы на тое, што практычна вся работа, якая не ўскладнюе процес атрыбутавання, лячыцца падэйсамам: слухачы AppState і NetInfo, запускаванні эвэнтаў Keyboard, стежыцельнікі за месцаўпадзелам, з’яўленні канектацый WebSocket, а таксама натыўныя SDK-ы для прыбороў уродзіце камеры чы Блэтузу.
Чаму цяжка адмовіцца ад яго выкорыстання
Падэйсамы патрэбуюць месца для сваёй роботы пасля завершэння атрыбутавання, а таксама месца для ўсунення перш чым компонент будзе знішчаны чы перш чым падэйсам зноў будзе запускацца. Без такой структуры вы патрапляеце да ситуацыі, калі слухачы застаюцца актываўнымі, падпісвы дублююцца, а клозуры стаюць неактуальнымі, і гэтыя проблемы каштуюць набагато больш, чым useEffect, калі яго викорыстоўваць правільна.
Калі варта вярнуцца да useEffect, а калі — ні
Хорашыя прыклады выкорыстання ў дапрацоўках на React Native включаюць:
- Асцэнаванне звуку з адпрацоўкі вялікіх джэрел дадзення, такіх як
AppState,NetInfo,KeyboardчыстаDimensions - Работа таймераў, інтэрвалоў чыста цыклаў анімазіі толькі калі экран ёсць видны
- Спаводжэнне локальнага стану у адпаведнасці з параметрамі чыста глобальным храненнем дадзення пасля завершэння адрасавання
- Загрузка дадзенняя пад час старту прыстрою, чыста знову калі змяніцца даныя, напрыклад ID чыста нынешняга пользователя
- Кераванне адпрацоўкай вялікага модуля імператыўным спосабам, напрыклад увімкненне/выключэнне тракінгу месцазнаходжэння, сканавання BLE чыста стріму камеры
Ситуацыі, калі эфект ёсць неправым інструментам:
- Адгэтуванне значэння з параметраў чыста стану. Яго трэба вырахаваць пад час адрасавання.
- Рэакцыя на дзеянне пользователя, напрыклад нажатчыку на кнопку. Яго трэба обрабоцваць у працоўніку змагання, а не ў эфекте, які стежыць за змянамі стану.
Ёсць болей дакладныя інформацыі пра анти-патэрн сінхронізацыі — прачытайце нашу статыю па чым сінхронізацыя стану з useEffect ёсць рызыкаванае.
Баг: экран для стежэння, які выкарыстоўваў усю батарэю
Уявіце сабе экран для стэйкінгу доставкі: карта, на якой рэальна адбываецца апдэйт месцаположэння водзіла, падобна да дапыткі службы доставкі ўжоў. Ён працаваў у сімулятары, праяшчыў пераканальнэ теставанне на двух-трох прыстроях і быў выпусцаны. Прыблізна чераз два тыдні пачалі прыходзіць запиты на падтрымку:
- У Android-прытку корыстнікі адзначалі, што тэлефон гарячыеў, а ўтрамбованасць батарэі знижвалася прыблізна на 15% за 20 хвілін пасля таго, як экран трэйсінгу заставалі ачытым.
- У iOS-прытку корыстнікі скаржыліся, што карта працавала з перашкодамі: маркер водзіла прыбегаў з адной пазіцыі ў іншую замест таго, каб рухацца плавна, а прасуванне было медзленым.
Выявілася, што два разныя симптамы маюць адну і тую ж прычыну.
Компанента, спроставаная
Ёсць спроставаная версія экрана. Яна зберагае месца розташоўкі водзіла і статус замовлення, стварае з’єднанне через сокет, падпішываецца на абнаўленні месца розташоўкі ў адным ефекте і перыякруцьвае прыбутковы час у іншым. Две пазначаныя лініі — гэта там, дзе былі проблемы:
function TrackingScreen({ orderId }) {
const [driverLocation, setDriverLocation] = useState(null);
const [order, setOrder] = useState(fetchOrderSync(orderId)); // returns a new object reference
const socket = useMemo(() => connectSocket(), []); // looked memoized, wasn't the issue
useEffect(() => {
const subscription = LocationSocket.on('update', (loc) => {
setDriverLocation(loc);
});
return () => subscription.remove();
}, [order]); // 🚩 the bug
useEffect(() => {
console.log('Recalculating ETA...');
calculateETA(order, driverLocation);
}); // 🚩 no dependency array at all
return <Map driverLocation={driverLocation} order={order} />;
}
Два незалежныя проблемы былі нарадзеныя адна ўздоўж другой.
orderатрымала новую ідэнтычнасць об’екта кожны раз, калі выканана рэндарызацыя яго ўзростку. У рэальным працоўніку гэта выходзіла з хука, які знаходзіцца вышэй у іерархіі, і які ў кожны раз ствараў новы об’ект з параметрамі. (У спрасцаванай часткі коду показана, што гэта выходзіць зuseState, які на самай працоўніку зберагвае стабільны кансэрт; спрыймайце гэты рядок як замену для вышэй распакаванага хука.) Пакалі эфект падпіску перыяднаваўorderяк залежнасць, React выкананаючы процедуру чысткі і знову падпісваўся на сокет месцазнаходжэння пасля кожной рэндарызацыі, а не толькі тады, калі значэннеorderсправжняя змінювалася.
setDriverLocation унутрь першага эфекта. У гэтым дапытчыкі вычысленне ETA таксама спрычыняла апдэйт стану ў іншых частках, што стваралаць цыкл: апдэйт месца, паўторнае атрыбутавання, эфект ETA, ўжо аднолькі апдэйт стану, зноў паўторнае атрыбутавання і так далей.Той самы код даў разныя сімптамы на розных платформах. На Android сокеты перыядычна раз’єднваліся і знова падключаліся, што прымусвала радіо і CPU працаваць практычна без перапынення; гэта і была справжня прычына швыдкага спрабавання батарэі. На iOS актывацыя радіо контролвалася інакш, але сталячы цыкл падпісоўвання і атрыбутавання продовжваў навантажваць поток JavaScript і змушваў карту значна частая перзапускваць шар з маркерамі, чыяе наследкам для корыстувачаў былі перерывы ў роботе.
Дапамога па гэты фрагмент: useState(fetchOrderSync(orderId)) вызывае fetchOrderSync праз кожны рендер, нават як React выкарыстоўвае толькі рэзультат першы раз. Якщо пачатковая значэнне выкарыстоўвае багато ресурсаў, лепш перадаць функцыю, як у useState(() => fetchOrderSync(orderId)), тады яна будзе выканана толькі адназначы.
Як была дыагноставана проблема
Шаг 1: Паказаць, што адбываецца перы-рэндары, а не проблемы з бібліятэкай map
Першым падазроўным была бібліятэка map SDK. React DevTools Profiler, який падключаецца да аплікацыі React Native чераз тую ж сетчатку Metro, якая выкорыстоўваецца для разработкі, быстра адмовіў у гэтым. Команда зафіксавала профіль на 10 секунд, калі экран з адстэйканням быў паказаны без жадных дзеянь.
У записі было показана, што дрэўо складовых адбывала дзесяткі рендароў за секунду, тады как бэкэнд апусціл новую локацыю драйвера толькі прыблізна кожныя 3–5 секунд. Гэта неадпаведнасць стала першым рэальным парадкам. Як правільна запамятовальная правіла, частота рендароў павинна адпавядаць значным змянам дадзэнняў; калі колькасць рендароў значна перавышае колькасць апдэйтаў, значыць, якісь фактар штучна іх спрычыняе.
Шаг 2: З’ясаваць, чаму відбываюцца падзвойныя рендары
У рангаванай візуальнасці Профілера было паказана, што TrackingScreen і Map адбываюцца адна за другойя, паступова. Чыба з’ясавіць точны фактар, была тымчасова дадзена малая бібліятэка для дыбаггінгу why-did-you-render. Яна фіксуе, якая змяненае властывасць або стан спрычыніў кожны рендар, і ёй удалося зафіксаваць следзэ:
TrackingScreen re-rendered because of changed props: order
order: Object !== Object (deep equal: true)
Фрагмент «deep equal: true» быў важлівым доказам. Контэнт order не зменіўся ніяк значным чынам; зменілася толькі його ссылка, адтуды што об’ект перзначалі на вышэйшых роўнях практычна ў кожны раз. React пораўнюе залежнасці за дапамогай Object.is, таму структурна ідэнтычны, але новарадзены об’ект завжды вважаецца змянай.
Шаг 3: Спостерагаць за натыўным бокам
Профілюванне JavaScript пояснюе процесы атрыбутавання, але не тое, што робіць сетевая апаратура прыстрою. На натыўным боку для спостерагдзення за жыццёвым циклам WebSocket быў выкарыстоўваны Flipper з плагінам Network і спецыяльным плагіном для логавання. Лог выказваў паўтарыяеся ўдачы connect і close за лічбу секунд, а не стабільнае з’єднанне працысла ўвесь час, пакуль экран быў ачынены. Це падтвердзіла, што сокет роз’ўязваўся ў кожны раз, калі эфект запускаўся занова.
Дыягностык Hermes ад Flipper даў ўсё большую падтверджэння: тыпавыя точкі зупінкі, размешчаныя ў функціях чысткі пасля выкарыстання прынтэпса, запускаліся значна частае, чым могла б паспяшыць рэальная дезактывацыя прынтэпса або змена замовлення.
Важны заўважэнне: новейшыя версіі React Native перасталі выкарыстоўваць Flipper як стандартны інструмент дыягностики на кантэксте React Native DevTools, таму пераканайцеся, што вы чыталі актуальную дасведчовую документацыю React Native, каб дазнацца, якая настройка рэкамендуецца для вашай версіі. Падход, які заключаецца у стазіроўванні жыцёвага циклу з’яносу та стварэнні точак зупінкі ў функцыях чысткі, праўдзівы для будзь-якіх інструментаў, якія вы выкарыстоўвайце.
Шаг 4: Аблікаванне рэальнага вплыву на акумулятар і CPU
На завершанне Profiler у Android Studio, выкарыстоўваючы сваія вікны CPU і Energy разам з Flipper, дазволіў канкрэтна абліковаць ступень падташчэння:
- Даўжэйшы час працы перад выправленнем: стабільны расход CPU ад 35 да 40%, калі экран для стежэння заставался нерухомым, а працэсаналізатор энергіі класіфікаваў дапыт як аплікацыю з вялікім расходам акумулятора чераз сталую радыяўскую актывацію.
- Пасля выправлення: расход CPU у стане спакою знизіўся да прыбліжна 4-6%, і працэсаналізатор энергіі больш не фіксаваў сталую радыяўскую актывацію. Ён паказваў короткія, періядычныя спалахі, якія збігаліся з рэальным інтэрвалам апдэйта.
Выправленне: тры цільаваныя змены
Кожная змена стосуецца аднаго з ланкаў у ланцужку.
1. Адсылацца да прымітів у замен на об’екты
Падпіску трэба перазапускаты толькі тады, калі самы порядак змінюецца, і рошт orderId точна гэта ідентыфікуе. Паколькі прымітівы параболююць за значэнням, ўсе іх значэнні застаюцца аднаковымі праз усе перадрукаванні:
useEffect(() => {
const subscription = LocationSocket.on('update', (loc) => {
setDriverLocation(loc);
});
return () => subscription.remove();
}, [orderId]); // orderId is a primitive string — stable across re-renders
2. Акцэнаваць залежнасці ад кожнага эфекту
Абсалютна неабярклівасць, калі вы сапраўды хочаце, каб эфект запускаўся пасля кожнага атрыбутавання, што бывае рэдка. У такім случыку час атрыбутавання павінен пералячыцца, калі зменяецца месца розташавання водніка:
useEffect(() => {
calculateETA(order, driverLocation);
}, [driverLocation]); // only recalculate when location actually changes
Строга кажучы, эфект таксама чытае order, таму правіло лінтингу react-hooks/exhaustive-deps будзе выкарыстоўваць яго ў массиве. Калі order будзе зберагацца у памяці (у наступнай праваке), яго дадаўшы будзе безпечна, і эфект застанется адрадным, таму што ён запускацца будзе толькі тады, калі порядак сапраўды зменіцца. Ігнораванне значэння, якое чытае эфект, можа прывести да вычыслення часу атрыбутавання на базе застарэлых дадзенняў.
3. Зберагаць об’ект order у памяці ў вышэйшых роўнах
На завершанне, стабілізаваць об’ект там, дзе ён ствараецца, так, каб яго аб’ект-пасылак зменяўся толькі тады, калі зменяюцца значэння важлівых поль:
const order = useMemo(() => buildOrder(rawOrderData), [rawOrderData.id, rawOrderData.status]);
Зусібудь уважна даўжынка спісу залежнасцяў: калі ў яму толькі id і status, змяны ў будзь-якім іншым поле rawOrderData, такім як адреса доставкі, не створуюць новага order. Гэта будзе правільна толькі тады, калі нічы не залежыць ад гэтых іншых поль.
Пасля внесення ўсіх трох прымэн, калі сокет быў падключаны толькі адна раз на кожны візіт на экран, прыбліжная дата прибутку перасчылялася толькі тады, калі месца розташавання фактычна змінювалася, а частота відрисовкі зменілася з дзесяткаў за секунду на прыблізна адну кожныя калькі секунд, што падтрымліваў рэальны поток даных.
Урокі для команд React Native
- Працаваць з гадзенням, што залежнасці ад об’ектаў і масэвых значэнняў є нестабільнымі. Якщо вы іх не створылі за дапамогою
useMemoабоuseCallback, чакайце новага кансэльнага значэння праз кожную перрадзіцю. Вярней выбіраць прымітныя залежнасці, такія як ID, калі гэта адпаведае меты. - Заўжды пісаць масэвую лісту залежнасцяў і не выключаць правіла перагляду коду. Функцыя
react-hooks/exhaustive-depsстворана саме для выяўлення такіх багоў. Їё выключэнне без розумення паведамлення пра баг є прычыной таго, што такія проблемы потрапляюць у продакшн; краща усунуць нестабільнасць. - Аналізаваць стан паказвальных экранаў, калі тыя не адпаведаюць дзействам, а не толькі дзейства самыя. Штосьць такога бага з’явілася толькі тады, калі ніхто не чапаўся за экран, і гэта самэ тое стан, які часта працягваецца заўважыць пад час ручнага тэставання.
Якщо вы хочаце глыбэй разабраць кантакты з дадзенымі па часе выкарыстоўвання, наша статыя пра распашчытаныя моделі, якія спрычыняюць непатрэбныя перывыкарыстоўвання дадзеных у React рассказвае пра адпаведныя проблемы.
Заключэнне
Мало якія функцыі ў React такія простыя у напісанні, як useEffect, і мало якіяя ў той жа час такія, пры якіх легка таямна памыліцца. На экранах у рэальным часе памылка ў залежнасцях не проста стварае дапаможную лінію ў журнале: гэта можа заставіць процес працаваць, перазапрацаваць поток JavaScript і зробіць дапрыяж працюваць некоректна без явных памылак. Стабільныя залежнасці, чысткія масівы і аналіз стану без дзейнасці — гэта простыя звычкі, якія запобегаюць найболей серйозным наследкам гэтых памылак.
Спадні матэрыялы
- Утрата памяці у React Native: аналіз структуры JS Heap і власнікаў памяці у натыўных компанентах — Дазвольце дазнацца, чаму процес збіркі сметлівага каштоўнасці не можа захаваць дапрыяж React Native ад утраты памяці у натыўных частках, і як з’ясаваць, што заставляе калбэкі, об’екты JSI і декодаваныя зображэння працаваць.