Пошук прасачаў памяці ў JavaScript: досяжнасць, утримвальнікі і чыстка.
Дазвольце дакласці, чаму JavaScript з механізмам разыбрання кашлакоў все ж такі прасачае памяць, якія ўзвычайныя моделі заставляюць память застацца, і як знайсці прычыну за дапамогою кадраў хіпа і ланцаў утримвальнікаў.
Веб-дзеянне, якое працюе швытка о 9 гадзін ранку, але стае повольным о 5 гадзін вечарам, ўскладненне знаходзится сярод найбольш распашчыхся сімптамаў выцэрпвання памяці: клікі адпавядаюць з запазджэнням, прасуванне тыражоў вяршыць без гладкасці, анімазіі спаўняюцься з перашкодамі, колькасць памяці RAM праходзіць за гігабайт, а падчысленне стораніцы знову усё налагоджвае. Такія выцэрпвання не ствараюць ніякіх асаблівасцей, не парабалююць тэсты і спраўна праходзяць практыку CI, таму што яны прояўляюцца толькі тады, калі хтось заставляе дзеянне працаваць гадзіны. У гэтым кярыранты раз’яснюецца, чаму нават мовы з функцыяй сбора сметлі все ж такі выцэрпваюць памяць, аналізуюцца моделі, якія вызываюць большасць рэальных выцэрпванняў, і прадаёцца алгорытм работы з інструментамі Chrome DevTools, які дапамагае знаходзіць точны элемент, який утримвае памяць жывой.
Сбор сметлі звалняе тое, што недаступнае, а не тое, што не выкарыстоўваецца
Паколькі JavaScript ніколі не прасіць вас вызваць malloc() чыраз free(), ёсць спадчына супакоўвацца, што система выканання абавесціваеся памяцю цалком. Гэта супакоўванне ўсьмо толькі часткова праведзя. Колектар не звяртае об’ект, калі ваш код заканчыў з яным працаваць; ён звяртае об’ект, калі да яго больш нічога не можа дасягнуць. Якшо якась забутая ссылка ўсё ж такі паказвае на об’ект, двыжок не мае можлівасці зрозумець, што гэты об’ект є непатрэбным. З точкі зору системы выканання, все, да чаго можна дасягнуць, могае ўсё ж такі быць патрэбным.
Гэты прасвет межа «больш не выкарыстоўваецца» і «больш не можна дасягнуць» — самэ там, дзе з’яўляюцца некалькі з найважэлейшых багоў у выдатнасці ў сучаснай разработцы веб-сайтаў.
Сцэнарый: панель керування, якая сповалілася ў кожны чарвенак
Уявіце панель керування аперацыямі, яка персонал залишае відкройленай працаваючы цэлы змяну. У режыме QA ўсё працюе швайна: быстрая пачатковая загрузка, эфектывныя запыты API, высокія балы Lighthouse. Але потым прыходзяць адказы з рэальнага сервісу. Чераз пяць-шасць гадзін переключэнне вкладакоў стае повольным, графікі перазрисовваюцца медленна, а нават простая модальная вікно патрабуе значны час, каб адкрылася.
Тыповая першая рэакція — зваліць віну на бэкенд. Команда налаштоввае запыты да базы дадзеных, пераканаліваецца, што адказы API залишаюцца менее 100 мілісекунд, і бачыць низкую загрузку CPU на серверах. Нічга з гэтага не можа поясніць сповольненне, якое прыбывае з часам.
Разлам адбыўся за дапамогай менеджера задаў Chrome. Размер карточкі ўранцы станавіў пра 150 MB, а да пачатку вечара вырасіў да майже 1,4 GB. Кожная навігацыя, кожны дыалог, кожна апштучка павтарнага завантажэння залишалі некалькі байт памяці. Кожна выдача памяці была мінімальная; але тыя, што былі тысячы, — нямінімальныя. Процес атрыбутавання, затрымкі ў сецесіі та швальнасць выканання былі ў норме. Аплікацыя проста ніколі не звалічвала об’екты, якія ёй больш не былі патрэбны.
Як двыжакі выбіраюць, што застаецца
Стварэнне об’екта ў JavaScript не выклікае жадных спецыяльных процедураў:
const user = {
id: 101,
name: "Emma"
};
Калі нічыя больш не аб’язана да выкарыстоўваць user, двайнер можа яго звярнуць. Цікава частка — гэта як ён гэта рашыяе. V8 (Chrome і Node.js), SpiderMonkey (Firefox) і JavaScriptCore (Safari) адзін у адзін стежыць за графам об’ектаў, з’яеднаных чераз аб’язаны. Корэны, такія як глобальны об’ект, знаходзяцца на верхнім роўні, а ўсё, што стварае ваша прыкладная програма, залежыць ад яных: інстанцыя прыкладной программы, маршрутызатор, храніліща дадзеных, дрэвы компонентаў і ўсі глобальныя зменныкі.
Цыкл збору пачынаецца ад гэтых корэнаў і следуе за кожным аб’язаным, які толькі можа. Усё, да чаго ён дасягае, застаецца. Усё, да чаго ён не можа дасягнуць, становіцца кандыдатам на ачыску. Ключоўтыя слова — кандыдат і недасяжны. Об’ект не ўключаецца у процес збору таму, што ён стары, не выкарыстоўваны чы забуты. Ён уключаецца толькі тады, калі няма жаднага шляху аб’язаных, які вядзе да яго.
Падзірце, якшо вы завантажыце вялікі список з записаў:
const employees = fetchEmployees();
Чырвоны час паэлемент UI больш не адображае гэтыя даны, але іншы об’ект яшчэ выказвае на яе:
cache.employees = employees;
Нават якшо ніхто больш не чытае cache.employees, калектар не можа гэтага прыпускаць. Аднасць існуе, таму весь масіў застаецца ў памяці. Апарат працуе абоўсумова так, як запланавана; вытэк выходзіць з коду прыемленае.
Мышленне па аднасцях заместо об’ектаў
Вытэкі становяцца набліжна простейшымі да разумення, калі вы перестаёте спрашваць пра об’екты і пачынаеце спрашваць пра аднасці, якія на яе выказваюць. Возьміце функцыю, якая стварае об’ект і вяртае яго:
function createUser() {
const user = {
name: "Alice"
};
return user;
}
const employee = createUser();
Пасля ўжывання яна, адна аднасць з’яўляецца між зменнай і об’ектам:
employee
│
▼
{ name: "Alice" }
Якшо пасля чаго зніць гэтую аднасць, у об’екта не будзе вхідных аднасцей, і наступны калектар зможа яго адмахнуць:
employee = null;
Адзін парадакт, калі вы спробуеце гэта самі: у паказваным раней фрагменте змянная employee была адзначана за дапамою const, таму перазначэнне яе выклікае TypeError. Адзначайце яе за дапамою let, калі вы плануеце пазней паспрабаваць змініць яе. Суть застаецца тая ж: самэ ўсуненне паследней ссылкі яшчэ больш робіць об’ект придатным да збору.
Тепер часткова зменіце прыклад, каб функцыя зберагала створаны яе об’ект у масэ на рэвэлі модуля:
const users = [];
function createUser() {
const user = {
name: "Alice"
}; users.push(user);
}
Калі createUser() завершыцца, кожны об’ект пользователя все ўсё залишаецца доступным через масу users, а сама маса є доступной з верхньага рэвэля:
Window
│
▼
users
│
├── User 1
├── User 2
├── User 3
└── User 4
Колікве users залишаецца доступным, то і кожны элемент у ям ўсё таксама доступны. Самэй гэтага вылівы памяці зазвычай расту поступова: жаданага великог об’екта няма, але тыясячы маленькіх об’ектаў накаплююцца за гадзіны або дні.
Вылівы памяці накаплююцца па адной взаімадзеі
Фраза "вылів памяці" часта асоціюецца з вельмі вялікім об’ектам, який спожывае сотні мегабайтаў. У практыцы ж такія ситуацыі зазвычай ўтвараюцца за дапамою маленькіх, павтараючыхся об’ектаў.
Уявіце сабе, што закрыць дыялогавое вікно налаштоўкаў заставляе за сабой прыблізна 20 KB. Гэта сама па сабе ўзятым можна не браць у расчыт. Але якщо аптымальны корыстнік ачынае і закрывае гэтае вікно 500 разоў за робочы дзень, ён вже втрачае або прасачвае бягам 10 MB. Якщо да гэтага дадаць пяць компанентаў, кожны з якіх прасачвае некалькі маленькіх кантэнтаў за кожную інтеракцыю, восьгадзіновую сесію, калькі карточак, якія застаюцца ачытымі адразу, і паведамленні, якія прыходзяць кожныя калькі секунд, то гэтыя незначныя кантэнты ператвараюцца на сотні мегабайтаў.
Гэтыя расчыткі таксама пояснююць, чаму разработчыкі рэдка калі це зазначаюць. Пад час разработкі сторанку перзаваняюць кожныя калькі хвілін, чым усё скасываецца. Корыстнікі ж не перзаваняюць сторанку; яны працуюць далей.
Чаму самэ прыкладнікі з адной сторанкай больш усвядомляюць гэты проблема
Класычныя веб-сайты з калькамі сторанак мелі нехтацыйны захоўнік: кожная навігацыя завантажала чысты дакумент і абрасвала весь кашт JavaScript, укладаючы ў гэта і прасачваныя об’екты.
Прыкладныя програмы з адной сторанай, створаныя за дапамою фреймворкаў такіх як React, Angular, Vue, Svelte чыста падобных ім, можу працаваць гадзінныя часы без павальнае загрузкі. Гэта чудова для карыстальніка, і самэ гэтая прычына, чаму так важна дисцыпліна ў выкарыстоўванні памяці. Кожная змена маршруту, адкрыцья модальнага вікна, паведамлення, адправленае через WebSocket, чыстка графіка — усё гэта стварае об’екты. Якщо іх не звярнуць належным чынам, яны застаюцца ў памяці столькі ж, сколькі і сама сторана.
У гэтым ёсць іронія: чым кращы досвяд, тым дэльней людзі застаюцца на сайте, і тым больш шансоў, што незначныя відхілы ў выкарыстоўванні памяці буду накапляцца.
Система калектавання памяці є складнай, а не ясновидной
Сучасныя двыжкі выкарыстоўваюць пошаговае і паколінчастае калектавання, адночасную маркаванне, компактаванне памяці і калектавання пад час перыядоў без актывацыі. Гэтыя методы робяць кераванне памяцю швыткім і непометным, але яны не можу выправіць логічныя бягы.
Уявіце сабе, калі вы падаеце каму-небудзь кнігу і ніколі не прасіце яе адзначыць. Яны не можу знайсці, што вы пра яе забылі, таму, па ўсіх прамахоў, вы яшчэ спадзяецеся, што яна будзе адзначыта ў будучыні. Рэферэнсы працуюць так сама. Калі ваша аплікацыя зберагае рэферэнс да об’екта, двайнер прыпускае, што гэты об’ект мае значэнне, незалежна ад таго, чыргава ваш код зноў да яго будзе прыходзіць чы ні. Ён не можа прачытаць вашыя намеры; ён толькі следуе за лініямі зв’язку ў графе.
Гэта змена практычна пераменяе спосаб дыбаггавання. Уместа таго, каб спытвацца, чаму механізм звалекання памяці не выплывае ёю, вы ставите болей продуктыўнае пытанне: што ще зберагае рэферэнс да гэтага об’екта? Практычна завжды самэль там і знаходзіцца баг.
Шаблоны, якія стояць за большасцю рэальных вытаканняў памяці
Калектар не зламаны; прасачы выходзяць таму, што код заставае сабе рэферэнсы, якія ўжо не патрабуюцься. Такія рэферэнсы рэдка калі выглядаюць падазроўна. Яны беруцца з звычайнага, прыемна выглядаючага коду, а не з экзотычных алгорытмоў чы багоў прыстрою для перегляду. Наступныя шаблоны охопляюць прасачы, з якімі вы найбольш верагатна станэце супрацоўваць у працоўным серавере.
Слухачы запуска, якія ніколі не адключаюцца
Слухачы запуска ўваходзяць у число найчастэйшых прычын прасачоў, особліва ў SPA-проектах. Їх настройка зазвычай ўсё проста: берэцца элемент і прыўязуецца адпаведны обробнік.
const button = document.getElementById("save");
button.addEventListener("click", saveDocument);
Пазней выкарбалакуець навігуець і кантакт знікае з сторынкі. Выдаленне элемента з DOM сама па сабе не перывае ўсіх асоцыяваных з яму кансаўтаў JavaScript. Якщо обрабнавач все ўсё заўважаны, а яшчэ што-небудзь дазволяе элементу чы обрабнавачу застацца доступным, як слухальнік, так і тое, што ён абрамляе, застаюцца ў памяці. Вытэк водаў выражаецца яшчэ сяродзе, калі слухальнікі прыўязаны да элементаў, якія існуюць дзеўяльна, такіх як window чы document, таму што такія элементы ніколі не зникаюць. Раўек — явна аназначыць обрабнавач:
button.removeEventListener("click", saveDocument);
У React, Angular чыра Vue трэба гэта робіць у фазе unmount чыра destroy жыцёвага циклу компонента. Корыстным прыкметам є спрытваўляць кожны вызов addEventListener() як зобав’язанне: калі вы яго дадаўце, трэба ведаць, калі і дзе ён будзе адключаны. Перадача сігналу AbortController калькам слухачаў і абранне яго адразу пад час розбіркі є зручным спосабам выпалнення гэтага зобав’язання для ўсіх слухачаў адразу.
Таймеры, які працуюць далей пасля заканчэння роботы экрана
Таймеры таксама «тэкучаюць» непазірна. Дашборд, які запытае пра новыя даны кожныя пяць секунд, можа выглядаць так:
const timer = setInterval(() => {
loadLatestData();
}, 5000);
Якщо корыстнік пакінуе сторанку, а інтэрвал так і не будзе анульаваны, функцыя-выклік продовжае выконваліцца на фоне. Яна заставляе свой клоузур жыць, а разам з яймо — будь-якія функцыі, зменныя чыста цэлыя экземпляры компонентаў, на якія цей клоузур сацягваецца. Калькі забутых інтэрвалоў можу займаць значна большую колькасць памяці, чым можна было б спадзявацца. Анульавайце іх, калі ўсё, што імі керуе, зникае:
clearInterval(timer);
Тая ж практыка прыменяецца да setTimeout() (чараз clearTimeout()) і requestAnimationFrame() (чараз cancelAnimationFrame()).
Выдэльеныя вузлы DOM
Выдэльеным вузлам ёсць элемент, які больш не ў складзе дакумента, але які все ж такі сацягваецца з боку JavaScript. Тыповы спосаб создаць такі вузел — запуск модальнага вікна і яго выдаленне:
const modal = document.getElementById("modal");
modal.remove();
Здаецца, што элемент зник, але якшо яка-небудзь зменная, масаў, клозура чы гэты об’ект стану яшчэ выказвае на той элемент, яго нельга збіраць, так сама як і його дзецяў. Аплікацыі, якія дынамічна ствараюць модалныя вікна, падказкі, выпадаючыя меню чы панелі прыемаў, ўскладненяюць ситуацыю. Кожны вузол є маленькім, але пасля сотняў взаімадзейнасцяў ад’юнкаваныя паддрэсі можу займаць значную колькасць памяці.
Клозуры, якія зберагаюць больш, чым трэба
Клозуры ўваходзяць у склад наймоцнейшых функцый JavaScript, але вони таксама спрыяюць нехтарактарнам збераганню памяці. Разглядзім фабрыку, якая выдзеляе великую масу перш чым вернуць функцію:
function createLogger() {
const largeData = new Array(100000).fill("data");
return function () {
console.log("Logging...");
};
}
Функцыя, якая вяртаеся, ніколі не выкарыстоўвае largeData. Тое, чы рэштуець гэты масіў, залежыць ад таго, як дварак представляе ўзвышэнны спектр. На практыцы V8 зберагае толькі тыя зменныя, якія фактычна абсыяруюцца якой-небудзь замыкальной функцыяй у тым спектре, таму гэты конкретны фрагмент зазвычай не заставляе масіў у спектре. Рызык падчас таго, калі другая замыкальная функцыя, створаная ў том жа спектре, выкарыстоўвае largeData: замыкальныя функцыі дзелюць аднаго об’екта контэксту, таму логгер з довгым часам жыцця таксама заставляе великі масіў застацца у спектре. Выкарыстоўванне eval унутрь спектра таксама змушвае дварак заставіць усё.
Нічыя з гэтых прычын не робіць клаусуры паслабнымі; сучасны JavaScript залежыць ад яных. Урок заключаецца ў тым, каб рашуча выбіраць, што можа бачыць функцыя з довгай трымкай. Якщо ёй патрэбна толькі адна значэнне, перадайце яе або скопіюйце замест таго, каб функцыя выкарыстоўвала цэлы об’ект або набор дадзеных. Невялікія корэктаванні меж дасягамоў можа значна зменшыць відварот памяці.
Кэшы без політыкы вывядзання
Кэшаванне захоўвае час, які бы пайшоў на павтарную роботу, але кэш, які толькі зрастае, — это проста повалка памяці з добрымі намерамі. Ось мінімальны прыклад мемаізавання:
const cache = {};
function getUser(id) {
if (!cache[id]) {
cache[id] = fetchUser(id);
} return cache[id];
}
Спачатку ён працуе добра. Чэрез шысь месцаў пасля запуску ён можа зберагчы соткі тысяч ейтаноў, за якімі ніхто больш не будзе запытвацца. Замест таго, каб кэш расліваўся без меж, варта рассмотрзець:
- Максымальны размер
- Тэрмін давершання дадзеных за часам
- Стратэгію вывядзання LRU (менш часта выкарыстоўваныя)
WeakMap калі кэш функцыонуе на адміністрацыю об’ектаў, чыя трываласц жыцця павінна вялікай мерой адначасова вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай мерой вялікай меройЗаўважыце, што WeakMap прымае як ключы толькі об’екты (або незарэгістраваныя сімвалы), таму ён не можа заменіць механізм LRU для кэшоў, функцыонуючых на адміністрацыю числовых ID, як у прыкладзе вышэй. Якщо хочаце падтвердзіць, як функцыонуюць слабыя кансаекты, адзірніце нашыя матэрыялы “Сімвалы, WeakMaps, Проксі і генераторы”. Кэш без стратэгіі выдалення на самай справе не ёсць кэшам; гэта стацыонарныя сховішчы.
Глобальныя элементы, якія жывуць вечна
Усё, што прыўязана да глобальнага простору імен, жыве столькі часо, сколькі сама аплікацыя. Гэта ўдобна, але таксама і рызыкавана. Колекцыя на рэвэлі модуля, падобная да гэтай:
let allUsers = [];
пастаўляецца ўсё больш і больш з кожным разам, калі дадаюцца новыя даны:
allUsers.push(...newUsers);
Якщо толькі яныя частыні коду не выкарыстоўваюцься для ўсунення зайвага, масіва проста становіцца большым. За месяцы розрабоцтва вялікія глобальныя об’екты часта станавяцца месцамі накоплення дадзеных. Калі вы расследуеце проблемы з продуктывасцю, глобальны стан ўзяты ў расчыт як адна з першых дзеўай, якія варта пераглянуць.
WebSockets і іншыя дзейнасці, якія трываюць даволі дзеўа
Функцыяны рэальнага часу часта выкарыстоўваюць WebSockets, і для ўтварэння такога з’єднання патрабуецца толькі адна лінія коду:
const socket = new WebSocket(url);
Памылка заключаецца ў тым, што з’єднанне не закрываецца. Адкрытый сокет продовжвае прымаць паведамленні, запускать калбэкі і зберагаць стан прыкладнай програмы пасля таго, як корыстувач перайшоў ў інша месца. Закрывайце з’єднанні, калі функцыянал, які імі керуе, больш не выкарыстоўваецца:
socket.close();
Тая ж правіла прыменяюцца да observable-элементаў, стрымаў, спецыяльных эмітэраў паведамленняў і будзь-якіх іншых форматоў падпіску, якія можуць трываць даволі дзеўа пасля таго, як іх корыстувальнік больш не выкарыстоўваецца.
Аднойчыны прынцып
Этыя прыклады з візуальнага баку выглядаюць разнымі, але у ўсіх іх є адна спадчая прычына: якась частка продовжвае зберагаць аб’ект, які ўжо не павінен быць доступным. Цяя частка зазвычай є аднам з наступных:
- Зарэгістраваны слухальнік
- Чакаючый інтэрвал або таймаут
- Дыяпазон замкнення
- Постаўляючыся растучы кэш
- Зменная на рэвэлі модуля або глобальная зменная
- Ачыты ўсёцо або іншая падпіска
Калі вы будете думаць пра аб’екты як пра рэферэнсы, пошук вытакі стане набагато простэйшым. У замяну на запитанне, чаму пам’ять продовжвае растаць, запытайцеся, што ўсё яшчэ трывае аб’ект. Гэта запытанне зазвычай ведае працоўна да самай вытакі.
Знаходжэнне вытакі раней, чым це зрабяць вашы корыстувальнікі
Знанне прычын ўжоць карысна, але ў рэальнай базе коду галоўны вопыт — гдэ самэй трапляецца атрыбуты. Большая прыкладка можа мець тыячы компанентоў і сотні слухачаў, пры чым об’екты ствараюцца кожную секунду. Згадкі рэдка калі дапамагаюць. Інструменты браузера ўжоць чудовыя; розніца крыўціцца ў тым, чы робіцца іх вядомае, адпаведная практыка, а не толькі выучэнне кожной функцыяй DevTools.
Паказваце, чы справды атрыбуты
Зростанне памяці не завжды значыць атрыбуты. Энжыны выдзеляюць памяць пад час роботы прыкладкі і звяртаюць яе праз процес сбору, таму здаровая прыкладка паказвае шаблон у вачынцы зубца:
Memory
^
| /\ /\ /\
| / \ / \ / \
|______/____\__/____\___/____\____ Time
Выкарыстанне памяці зростае пад час актывацыі прыкладкі і зменшаецца пасля кожнага сбору. Прыкладка з атрыбутамі паказваецца інакш:
Memory
^
| /\ /\
| / \ / \
| / \ / \
|_______/______\__/______\________
| /
| /
| /
|___________/________________ Time
Маленькі спады паказваюць, што процес збору відходаў працюе, але кожны такі спад знаходзится вышэй за пярэдні. Памяць ніколі не вяртаецца да свайго пачатковага рубежа, і гэта є першым рэальным знакам таго, што об’екты залишаюцца ў памяці. Перш чым робіць вывары, ручна запускайце процес збору відходаў (іконка кантэйнера для сметы ў панелях «Памяць» і «Працэсавасць»), адколькі рубеж, які выглядае высокім толькі таму, што процес збору ўсё яшчэ не адбыўся, не є прасачаннем памяці.
Крок 1: ачыніце панель «Памяць»
Chrome прысвячае памяці кальколькі інструментоў, але вам не патрэбны ўсія яны адразу. Ачыніце DevTools і перайдзіце на панель Memory. Залежна ад версіі Chrome вы пабачыце разныя типы аналізу, такія як знімак структуры памяці, інструменты для відстэйквання ресурсоў у часовай шкале і выбіранне прыкладоў выдзелення памяці; точныя назвы і опцыі зменяюцца між версіямі, таму, якщо у вас інша ситуацыя, пераканайцеся ў актуальной документацыі DevTools.
Для большінства розслідавань фотакула купкі є правильной віскі пунктом, таму што яна паказвае, што наразе займае пам’ять.
Крок 2: зафіксаваць базовы стан
Перад тым, як запрацоўваць падозрэлую функцыю, зробіце фотакулу пачатковага стану прыкладнення, таму што яна є фотаграфіяю купкі. Пасля чаго парадоксальнае дзеянне, якое вы падозрываеце, трэба адрабатваць раз за разам. Напрыклад:
- Ачыніце і закрыце модальне вікно дзесяць разоў
- Пераводзіцеся туды-сюды межу сторанамі
- Запрацоўваце завантажэнне файлу
- Застосавіце фільтры да большай табелі дадзенаў
- Парадоксальнае пераводзіцеся межу вкладкамі панелі керавання
Калі закончыце, зробіце другую фотакулу. Тады у вас будзе два станы для парабярання.
Крок 3: парабяраванне фотакулаў
Што самэе і ўсё пачынаецца тут – у рэгламентнай перацэўке. Якщо процедура чысткі будзе эфектыва, тымчасовыя об’екты, створаныя пад час взаімадзеі, должны зникнуць пасля ўсунення. Якщо ж ні, певныя типы об’ектаў продовжуюць зростаць. Тыпавыя падазроўнікі включаюць:
- Элементы DOM, якія былі ад’єднаны
- Вялікія масівы
- Прыемнікі запуску падзеяў
- Своі сабственныя класы прыкладнага праграміста
- Компаненты фрэймворку, якія малі быць знішчаны
Вам не трэба разумець кожны элемент у хіпе. Выкорыстоўваючы режым порэвання, шукайце типы об’ектаў, колькісна цана якіх пад час павтарэння той самай дзеянні зростае адносна стабільным чыннікам. Стабільнасць зазвычай ёст толькі найболей сильны пацёрк.
Элементы DOM, якія былі ад’єднаны, – гэта найпростейшыя доказы для выявлення
Адыякраваныя вузлы — адны з найпростейшых спосабоў выявіць вытэкі. Ачніце та закрыйце модальное вікно двадцать разоў; пасля кожнага закрыцьця гэтае вікно павінна з’явіцца. Якшчы ў кадры застаецца двадцать элементаў модальнага вікна, значы ўсё-такі ёсць што-та, што іх утримвае. Вы можете запісаць „Detached“ у фільтр класаў кадры, южы бы яны быстра павінні быць выказаныя.
Сам DOM рэдка бывае справжнім прычыной. Асалодныя прычыны зазвычай лежачы ў іншых месцах:
- Обработчык, які ўсё ўсё ж зарэгістраваны на элементе або на цялевым об’екте з дужа довгай трываласцю існавання
- Інтэрвал або таймаут, чыя функція-адаптавальнік згадвае вузел
- Клозура, якая зафіксавала элемент
- Храненне, поле компонента або масіва, якія збераглі паказвач да ўсьго гэтага
Вважайце адыякраваны вузел сімптамам: падтверджэнням таго, што якаясь іншая рэферэнцыя завадзіла ачысце.
Следзіце ланцуг утримальніка
Калі вы знайдзеце об’ект, які явнаўсабо не павінен больш існаваць, наступны вопыт — хто яго прыглушае. Раздзел Retainers у кадры хіпа даўодзіць адказ на гэты вопыт. Выберыце об’ект, і Chrome пакажае ланцуг справак, якія з’ўязуюць яго з коранем GC. Канцэптуальна, гэта можа выглядаць так:
Window
│
Application
│
UserService
│
cachedUsers
│
User Object
Тады розследаванне стае простым. Уместо таго, каб гадаць, чаму об’ект корыстніка застаецца, вы можетэ пабачыць, што яго справакуе колекцыя cachedUsers унутрь UserService. Аднаходжэнне прыглушанага об’екта ёсць карытным; аднак самэ глыбокае з’ясаванне таго, што яго прыглушае і є тым, што насправдзе вылечвае баг.
Аблікаванне лайв-показнікаў за дапамогою Performance Monitor
Кадры ідеальныя для деталізаванага аналізу, але яны не ўсё. Performance Monitor у Chrome паказвае лайв-метрыкі, якія включаюць:
- Размах кашы JS
- Колькісць вузлоў DOM
- Колькісць слухачаў запускаў JS
- Дакументы і рамкі
Якщо колькісць вузлоў DOM чы слухачаў продовжвае растаць праз тое, што вы павтараеце адну і тую ж дзеянне, значыча не выканана чыстка. Адною з пераваг ёсць швальнасць: вам не трэба чакаць, пакуль прыстрой стане повольным, а сумнівныя тэндэнцыі часта становяцца виднымі вядома за калькі хвілін.
Ізолюйце адны маленькі, можна-павтарымы сцэнарый
Частая памылка — спроба дакладна расследаваць всю аплікацыю заодно. Замест таго сфокусавацеся на адной взаімадзейнасці:
- Пакажыце адну модальную вікно, закрыйце яе і зробіце гэта двадцать разоў паспяльна
- Алеў жо, перайдзіце межы тых сабе двух маршрутаў пяцьдзiesять разоў
Такі складныя сцэнарыю значна простыя для кантыфікацыі. Калі адна паўтаральная дзеянне прыносіць рост у кожной ітерацыі, вы вялікай мерой звузілі дыяпазон пошуку, і адпаведны код зазвычай лёгка знайсці ў такім разе.
Свэдама теставайце длігія сесіі
Разработчыкі часта працуюю з дапрыемом дзесять або пятнаццац хвілін і пераходзяць да іншага. Рэальныя корыстувачы внутраніх панелей керавання, платформ для трынків, інструментаў монітарынгу або порталаў падтрымкі могу залишаць іх ачытымі цэлы дзень. Уключайце длігія сесіі ў тэсты памяці: залейце дапрыем ачытым, періядычна взаімаўцаваліся з яным і старанна стежыце за тым, як развіваецца памяць. Багато прычын вытэкання памяці стаюць виднымі толькі пасля сотняў аб тысячаў взаімаўцоў.
Процес дэбаггіну, які адмовяецца ад згадкавання
Пераходжэнне між інструментамі аналізу витрачае час. Фіксаваная последовасць зазвычай работае краща:
- Паказваце, чы памяць продовжвае расту ў разных колекцыях.
Это пазбяглівае зусім прыпущэнняў. У замяну на тое, каб прыпускаць, што вінаватыя адны конкрэтныя складовы, вы дазваляеце доказам паказаць вам іх.
Запобежчанне вытэкам прычымо да запуску
Дыягназа ўсьмо толькі палова прыбліжнае рашэнне; найякшы спосаб — ўвесь час утрымлівацца ад падачы воды. Большасць протэкаў не ёсць наследкам неправильнага разумення JavaScript-коду з боку разработчыкаў. Яны выступаюць таму, што сучасныя дапыткі працуюць дзеўяносто часоў, ёсць высока інтэрактывныя і постоянна выдзеляюць ресурсы, і ў такім сераўе лёгка забыць, што все, што вы ствараеце, зрэшты трэба знісці. Команды, якія рэдка сталкнуцца з проблемамі памяці, не обавязкова пішуць болей розумны код; у яных є прычыны, якія зменшуюць верагатнасць протэкаў.
Задаць кожнаму ресурсу чыста вялікі тэрмін службы
Кожны раз, калі код стварае ўстойчывы элемент, запытайце ся аднае пытанне: колі гэта будзе знішчана? Гэта стосуецца не толькі памяці:
- Аб’екты-слухачы на элементах,
windowчыdocument - Інтэрвалы, таймауты і кадры анімацыі
- Сокеты і іншыя сецесіі сеті
Настроўка гэтага зазвычай простая; але ў сферы чысткі програмы часта паспяшаюць. Є простая правіла: якщо у вашам кодзе є пачатак, то яму таксама трэба канец. Лячна гэтая падходжаць запобегае значным вылівам ресурсаў.
Змусіце компоненты самі чыставацься
Фрэймворкі, заснованыя на компонентах, спрыяюць стварэнню самодостатняых елементаў, і такія елементы павінны включаць функцыю чысткі. Компонент, який запускае таймер, павінен яго зупініць, калі ён перестае выкананы. Компонент, який рэгіструе слухачы, павінен іх адключыць. Нічто з таго, што запускае компонент, не павінна продовжваць працаваць, калі ён з’являецца за межама екрана.
Уявіце сабранне з гастронаму: вы не йдзеце, калі святлыя заўсёды увімкнутыя, тэлевізор працуе, а кран заўсёды відкрыты. Хораша складовая заставляе всё такім, якім яго знайшлі. У React гэта значыць неабходнае вярнуць функцію для чысткі з кожнага useEffect, який падпішыцца, заплануе або з’яеднае.
Проектаваць кэшы з урахованнем выдалення, а не толькі дадзення
Кэшы ствараюцца з добрымі намерамі — ўтримацься ад павторных вызоў API чы розрахункоў, якія займаюць багато часу. Чэрез калькі месцацоў ў іх можа быць тыячы об’ектаў, якія ніхто не адкрываў целыя тыдні. Калі праектаваеце кэш, думайце пра тым, як элементы выходзяць з яго такім жа адказвальным спосабам, як і прыходзяць:
- Як дазго элемент должен застацца ў памяці?
- Які максымальны розмер?
- Чы элементы должны автаматычна выгораць?
- Чы можна выдаліць рэдка выкорыстоўваныя элементы?
Якщо на гэтыя запитанні немае адказаў, кэш практычна напэўна будзе раставаць з часам.
Зберагаеце толькі тыя даны, якія вам действительна патрабуецца
Іншая частая проблема — зберагачча цэлых об’ектаў, калі выкарыстоўваецца толькі ўжо частка з іх. Якщо вы запрашаеце вялікі профіль толькі для показу імені пользователя, няма прычыны зберагаць усю адпаведзь незаканчэнна; зберагаеце толькі тыя поля, якія патрабуецца інтерфейсу. Меньшыя об’екты спрацоўваюць менш памяці, іх легчэ кераваць, і менш верагатыва, што яны застаюцца без нагляду. Інодзе рашэнням не ў адносе да большага коду, а ў адносе да меншай колькасці збераганых дадзеных.
Серьёзна ставіцеся да незначных вытакаў памяці
Хочацца прыгнучы ўвагу да вытакаў памяці у каніках кілабайтаў. Проблема ў тым, што корыстувальнікі рэдка калі чаго-небудзь рабяць толькі адной раз. Дашборд, які працуе цэлы дзень, внутршній інструмент адміністрацыі, які выкарыстоўваюць сотні спецавацэў, або панель монітарынгу, якую ніхто ніколі не перзаваняе, запускаюць тыя ж шляхі коду тысячы разоў. Вытак, які сегодні ледзь можна зафіксаваць, пасля калькоў нормальнага выкарыстоўвання можа стаць справжнім інцидэнтам.
Даўце перакананні ў каштоўнасці памяці ў кожны дзень разработкі
Работа над выдатнасцю зазвычай сфокусавана на часе загрузкі та затрымках API, але памяць таксама трэба врачымаць. Пад час стварэння новай функцыі пасветце калькамі часу на перакананні:
- Чы памяць вяртаецца да свага базовага рэвэлю пасля выкарыстоўвання функцыі?
- Чы колькасць слухачаў збіраецца нечакана?
- Чы вузлы DOM зникаюць пасля адключэння компанентаў?
- Чы павтарэнне той самай дзеяння постаўна збірае памяць?
Этыя перакананні ўжоць дышэ і можу заўсёды захаваць гады на дэбагаванні пазней.
Спіс перакананняў пад час адзору коду
Перш чым з’еднаць код, праходзіце праз короткі список:
- Чы кожны даданы слухач адбыўся таксама і адключыўся?
- Чы таймеры аброшваюцца, калі яны больш не патрабуюцца?
- Чы падпісанні распрацоўваюцца належным чынам?
- Чы гэты кеш можа растаць без меры?
Ня трэба прыменяць гэта да кожнай лініі, але ўключыць гэта ў процес перагляду значна зменшае шансы на выкліканне прасачоў.
Ключовыя выводы
- Прасачы рэдка калі спрычыняюць зупінку працы чаго-небудзь ў першы ж дзень; яны паступова накапліваюцца і спачатку паграждаюць самым актыўным корыстувачам, і гэта яшчэ больш робіць іх апаснымі.
- Їх таксама можна прыгадаць заздалегідь: об’ект застаецца жывым толькі таму, што яшчо є рэферэнсы да яго, таму кожны прасач мае свой аднаковы шлях, які ведае да рэферэнсу, які трэба было бы адмовіцца від яго.
- Калі праця дапрыгвы замедляецца працэсамі, не трэба карыць браузер чы двыжак. Зробіце знімак памяці, знайдзіце об’екты, якія застаюцца, і праследавайце ланцуг тых, хто яны зберагае.
- Звычныя адказы ўсё часта просты: забуты слухач, неасканаваны таймер, безмежны кэш або компонент, які так і не завершыў очыску.
- Швайны JavaScript — гэта не толькі шырока скорасць выканання; гэта таксама кераванне жыцёвым циклам таго, што вы выдзеляеце, каб аплікацыя заставалася чутлівайя да дзеянняў, незалежна ад таго, чыя людзі вжываюць яе пяць хвілін чы ўвесь рабочы дзень.
Спадні матэрыялы
- Утрата памяці ў React Native: аналіз JS Heap і власнікаў памяці на натыўнае рэверсіі — Дазвольце дазнацца, чаму сбор відходаў не можа захаваць аплікацыю React Native ад утрат памяці на натыўнае рэверсіі, і як з’ясаваць, што прычыніваеся да таго, што калебаціі, об’екты JSI і декодаваныя зображэння застаюцца жывымі.
- Кэш карт вывары Нода — глухая вытока памяці ў режыме разработкі — Дазвольце дазнацца, чаму актываўанне --enable-source-maps або NODE_V8_COVERAGE можа спрычыніць неактуальны рост хіпа з-за павтаральных вызоваў eval, і як сёньня діаганаваць і зменшыць гэты проблема.