Главная » Статьи » Устранение утечек памяти в приложении

Устранение утечек памяти в приложении

Устранение утечек памяти в приложении

От автора: утечка памяти может происходить по-разному. В любом приложении приличного размера происходит утечка памяти. Последствия этих утечек могут быть мягкими, они могут раздражать (медленное приложение) или они могут быть разрушительными, вызывая сбой во вкладках браузера. Вот почему так важно следить за потреблением памяти и применять шаблоны, предотвращающие утечки.

В этой статье мы рассмотрим два типа: утечки по слушателям событий и по интервалам / тайм-аутам. Я буду использовать React в качестве примера, но основные концепции применимы к любой платформе, которая занимается добавлением и удалением узлов DOM на странице.

Базовое приложение

Допустим, у вас есть базовое приложение, которое просто добавляет и удаляет дочерние компоненты, в данном случае компонент под названием Snoopy:

class App extends React.Component { constructor() { super(); this.state = { snoopy: null, }; } render() { return ( <div> <h1>This is an app</h1> {this.state.snoopy ? ( <div> <Snoopy /> <button onClick={() => this.setState({snoopy: false})}> Remove the Snoopy component </button> </div> ) : ( <button onClick={() => this.setState({snoopy: true})}> Add a Snoopy component </button> )} </div> ); }
}

Это синтаксис компонентов класса в React, но не волнуйтесь, функциональные компоненты и хуки появятся через секунду. Итак, это приложение достаточно простое, вы кликаете и Snoopy добавляется в приложение. Кликаете еще раз, и его больше нет.

Snoopy

А что делает Snoopy? Он отслеживает нажатия клавиш.

class Snoopy extends React.Component { constructor() { super(); this.state = { keys: [], }; } componentDidMount() { document.addEventListener('keydown', (e) => { const keys = [...this.state.keys]; keys.push(e.keyCode); console.log(e.keyCode); }); } render() { return ( <p> I am Snoopy. I have been snooping your keystrokes. <br />I am delighted in inform you that your console has a list of the key codes you pressed </p> ); }
}

Ничего особенного, правда? Но что происходит после удаления Snoopy из приложения? Он отслеживает нажатия клавиш. Несмотря на то, что Snoopy отсутствует в приложении и в дереве DOM нет его следов, функция слушателя все еще находится в памяти и все еще… отслеживает.

Подумаешь? Вы добавляете еще один экземпляр Snoopy, и он по-прежнему работает должным образом. Продолжайте добавлять и удалять его несколько раз, и теперь у вас есть проблема. Кстати, вы можете попробовать код из этой статьи здесь.

Это был пример того, что слушатель событий DOM является unchecked. Вы только что вызвали утечку памяти. И не было никаких указаний на то, что что-то пошло не так, ни ошибки, ни даже предупреждения в консоли. Что, если Snoopy также включает временной интервал?

class Snoopy extends React.Component { constructor() { super(); this.state = { seconds: 0, keys: [], }; } componentDidMount() { // task 1 const i = setInterval(() => { const seconds = this.state.seconds + 1; this.setState({seconds}); console.info(seconds); }, 1000); // task 2 document.addEventListener('keydown', (e) => { const keys = [...this.state.keys]; keys.push(e.keyCode); this.setState({keys}); console.log(e.keyCode); }); } render() { return ( <p> I am Snoopy. I have been snooping your keystrokes for{' '} {this.state.seconds} seconds. <br />I am delighted in inform you that so far you have pressed keys with the following codes: <br /> {this.state.keys.join(', ')} </p> ); }
}

Теперь, когда Snoopy удаляется из DOM, setInterval продолжает кликать и делать что-то. Функция, вызываемая setInterval, все еще жива и работает в памяти.

Этот пример, тем не менее, касается состояния в обеих функциях (обратите внимание на вызовы this.setState()). В результате, когда Snoopy удаляется из DOM (отключен), React выдаст предупреждение в консоли. И, надеюсь, разработчик обратит внимание на эти предупреждения, поскольку они отображаются только в процессе разработки, а не в производственной среде. Таким образом, это предупреждение можно легко пропустить в достаточно сложном приложении, которое много печатает в консоли. И это при условии, что предупреждение даже появляется, потому что помните, если функции с утечкой памяти не касаются состояния, предупреждений не будет.

Устранение утечек

Решение для этих утечек аналогичное, оно включает в себя уборку за собой. Если вы добавляете EventListener, вы должны удалить EventListener. Если вы устанавливаете Interval / setTimeout, вы должны удалить Interval / clearTimeout.

В случае компонентов React это означает использование метода жизненного цикла componentWillUnmount(). Для этого также потребуется, чтобы слушатель событий больше не был встроенной функцией. Также потребуется, чтобы идентификатор интервала хранился где-нибудь, откуда его можно было бы извлечь, для очистки. Что-то вроде этого:

class Snoopy extends React.Component { constructor() { super(); this.state = { seconds: 0, keys: [], }; this.keydownHandler = this.keydownHandler.bind(this); this.intervalID = null; } keydownHandler(e) { const keys = [...this.state.keys]; keys.push(e.keyCode); this.setState({keys}); console.log(e.keyCode); } componentDidMount() { // task 1 this.intervalID = setInterval(() => { const seconds = this.state.seconds + 1; this.setState({seconds}); console.info(seconds); }, 1000); // task 2 document.addEventListener('keydown', this.keydownHandler); } componentWillUnmount() { // task 1 cleanup clearInterval(this.intervalID); // task 2 cleanup document.removeEventListener('keydown', this.keydownHandler); } render() { return ( <p> I am Snoopy. I have been snooping your keystrokes for{' '} {this.state.seconds} seconds. <br />I am delighted in inform you that so far you have pressed keys with the following codes: <br /> {this.state.keys.join(', ')} </p> ); }
}

Демонстрация

Таким образом, у вас есть две настройки при монтировании и две соответствующие задачи очистки перед размонтированием. Если компонент большой, между двумя отдельными задачами и их очисткой может быть куча кода, так что будьте осторожны. Другая вещь, которая слегка раздражает, — это то, что эти задачи не связаны между собой, но их все же необходимо объединить в одни и те же методы жизненного цикла. Хуки устраняют эти два неудобства.

То же, но с хуками

При использовании хуков вам понадобится useEffect () для настройки таких «эффектных» задач, а также для устранения беспорядка, создаваемого ими. Слово «эффект» означает, что это побочные эффекты отрисовки компонента. Основная задача компонента — показать что-то на экране. Отслеживание времени и отслеживание кликов — побочные эффекты основной задачи. Но я отвлекся. Паттерн выглядит вот так:

useEffect(() => { // set stuff up, like `componentDidMount()` return () => { // clean things up, like `componentWillUnmount()` };
}, []);

Таким образом, наш Snoopy с хуками теперь выглядит так:

function Snoopy() { const [seconds, setSeconds] = useState(0); const [keys, setKeys] = useState([]); // task 1 useEffect(() => { const intervalID = setInterval(() => { setSeconds(seconds + 1); console.info(seconds); }, 1000); return () => { clearInterval(intervalID); }; }, [seconds, setSeconds]); // task 2 useEffect(() => { function keydownHandler(e) { const newkeys = [...keys]; newkeys.push(e.keyCode); setKeys(newkeys); console.log(e.keyCode); } document.addEventListener('keydown', keydownHandler); return () => { document.removeEventListener('keydown', keydownHandler); }; }, [keys, setKeys]); return ( <p> I am Snoopy. I have been snooping your keystrokes for {seconds}{' '} seconds. <br />I am delighted in inform you that so far you have pressed keys with the following codes: <br /> {keys.join(', ')} </p> );
}

Хорошие новости с точки зрения читаемости кода заключаются в том, что несвязанные задачи могут существовать в своих собственных мирах (также известные как вызовы useEffect ()), а также то, что код установки и удаления находится рядом друг с другом.

Заключение

Позаботьтесь об утечках памяти, приняв шаблоны, которые поддерживают уборку после себя. Утечки памяти, как правило, увеличиваются со временем, которое пользователь проводит на странице, так и в течение времени, когда ваше приложение разрабатывается и поддерживается. Не позволяйте приложению ржаветь. Не пишите больших приложений, в которых явно присутствует проблема с памятью, но устранить такие утечки затруднительно. Решение? Дополнительный тайм-аут обновляет страницу время от времени, чтобы сбросить все, что происходит.

Еще один совет: если у вас есть приложение React с компонентами класса, проверьте написание вашего componentWillUnmount. Убедитесь, что это не componentWillUnMount. Потому что, если метод написан с ошибкой, предупреждения не будет, и весь код очистки будет просто мертвым грузом. И, поверьте, это может случится с каждым. Так что проверьте это прямо сейчас!

Автор: Stoyan Stefanov

Источник: calendar.perfplanet.com

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен