Управление памятью JavaScript: как избежать утечек памяти и повысить производительность

Содержание

  • Введение
  • Как управлять памятью в JavaScript
  • 1. Сборщик мусора
  • 2. Стек и куча памяти
  • Общие причины утечек памяти
  • 1. Циркулярная ссылка
  • 2. Слушатели событий
  • 3. Глобальные переменные
  • Лучшие практики ручного управления памятью
  • 1. Слабые ссылки
  • 2. API сборщика мусора
  • 3. Снапшот кучи и профилировщики
  • Заключение

Введение

Будучи веб-разработчиком, вы знаете, что каждая написанная строчка кода может влиять на производительность приложения. А когда речь заходит о JavaScript, управление памятью  —  один из важнейших моментов, на которые следует обращать внимание.

Только подумайте: каждый раз, когда пользователь взаимодействует с сайтом, он создает новые объекты, переменные и функции. И если вы пустите все на самотек, эти объекты будут накапливаться, засоряя память браузера и замедляя работу пользовательского интерфейса в целом. Представьте себе этот процесс в виде образования пробки на информационной магистрали: наверняка такая помеха оттолкнет многих пользователей.

Но так не должно быть. Владея необходимыми знаниями и методами, вы можете установить контроль над памятью в JavaScript, обеспечив бесперебойную и эффективную работу приложений. В этой статье мы рассмотрим все тонкости управления памятью JavaScript, а также расскажем о распространенных причинах утечек памяти и стратегиях их предотвращения. Будь вы профессионал или начинающий JS-разработчик, вы лучше поймете, как писать экономный, эффективный и быстрый код.


Как управлять памятью в JavaScript

1. Сборщик мусора

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

Для управления памятью сборщик мусора использует технику “маркировка и очистка”. Сначала он помечает все объекты, которые еще используются, затем “подметает” кучу и удаляет все непомеченные объекты. Этот процесс выполняется периодически, а также когда в куче не хватает памяти, чтобы обеспечить максимально эффективное использование памяти приложением.

2. Стек и куча памяти

Когда речь заходит о памяти в JavaScript, как правило, упоминают о двух основных хранилищах: стеке и куче.

Стек используется для хранения данных, которые нужны только во время выполнения функции. Он быстр и эффективен, но вместе с тем имеет ограниченную емкость. Когда вызывается функция, движок JavaScript помещает переменные и параметры функции в стек, а при возврате функции он снова их вытаскивает. Стек используется для быстрого доступа и такого же быстрого управления памятью.

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


Общие причины утечек памяти

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

1. Циркулярные ссылки

Одной из наиболее распространенных причин утечки памяти являются циркулярные ссылки. Они возникают, когда два или более объекта ссылаются друг на друга, создавая цикл, который не под силу разорвать сборщику мусора. Это может привести к тому, что объекты будут храниться в памяти долгое время после того, как перестанут быть нужными. Пример:

let object1 = {};
let object2 = {};

// создание циркулярной ссылки между object1 и object2
object1.next = object2;
object2.prev = object1;

// действия с object1 и object2
// ...

// установка object1 и object2 в null для отключения циркулярной ссылки
object1 = null;
object2 = null;

В этом примере мы создаем два объекта (object1 и object2) и циркулярную ссылку между ними, добавляя к ним свойства next и prev. Затем устанавливаем object1 и object2 в null, чтобы отключить циркулярную ссылку. Но поскольку сборщик мусора не может ее отключить, объекты будут храниться в памяти долгое время после того, как в них отпадет необходимость, что приведет к утечке памяти.

Чтобы избежать подобной проблемы, мы можем использовать технику “manual memory management” (“ручное управление памятью”), задействуя ключевое слово JavaScript delete для удаления свойств, создающих циркулярную ссылку.

delete object1.next;
delete object2.prev;

Другой способ борьбы с такими типами утечки памяти  —  WeakMap и WeakSet, которые позволяют создавать слабые ссылки на объекты и переменные. Подробнее об этой методике расскажем чуть ниже.

2. Слушатели событий

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

let button = document.getElementById("my-button");

// прикрепление к кнопке слушателя событий
button.addEventListener("click", function() {
console.log("Button was clicked!");
});

// действия с кнопкой
// ...

// удаление кнопки из DOM
button.parentNode.removeChild(button);

В этом примере мы прикрепляем слушатель событий к элементу кнопки, а затем удаляем кнопку из DOM. Несмотря на то что элемента кнопки больше нет в документе, слушатель событий все еще прикреплен к этому элементу. Таким образом, создается ссылка на функцию слушателя, что не позволяет сборщику мусора освободить память, используемую элементом. Это может привести к утечке памяти, если функция слушателя не будет удалена, когда элемент окажется ненужным.

Чтобы не допустить подобного типа утечки памяти, важно удалять слушатель событий, когда элемент больше не нужен:

button.removeEventListener("click", function() {
console.log("Button was clicked!");
});

Еще один вариант  —  использовать метод EventTarget.removeAllListeners(). Он удаляет все слушатели событий, добавленные к исходному элементу, на котором произошло событие.

button.removeAllListeners();

3. Глобальные переменные

Третьей распространенной причиной утечки памяти являются глобальные переменные. Когда вы создаете глобальную переменную, она доступна из любой точки кода, что затрудняет определение момента, после которого она больше не нужна. Это может привести к длительному хранению переменной в памяти после того, как она перестанет быть нужной. Пример:

// создание глобальной переменной
let myData = {
largeArray: new Array(1000000).fill("some data"),
id: 1
};

// действия с myData
// ...

// установка myData в null для отключения ссылки
myData = null;

В этом примере мы создаем глобальную переменную myData и храним в ней большой массив данных. Затем устанавливаем myData в null, чтобы отключить ссылку, но поскольку переменная является глобальной, она все еще доступна из любой точки кода, и трудно определить момент, когда она больше не нужна. Это может привести к длительному хранению переменной в памяти после того, как она больше не нужна, что вызовет утечку памяти.

Чтобы избежать утечки памяти такого типа, используйте технику “Function Scoping” (“Определение области видимости функции”). Она заключается в создании функции и объявлении переменных внутри этой функции так, чтобы они были доступны только в области видимости функции. Таким образом, когда функция больше не нужна, переменные автоматически собираются в мусор.

function myFunction() {
let myData = {
largeArray: new Array(1000000).fill("some data"),
id: 1
};

// действия с myData
// ...
}
myFunction();

Еще один вариант  —  использовать в JavaScript let и const вместо var, что позволяет создавать переменные с областью видимости на уровне блока. Переменные, объявленные с помощью let и const, доступны только в пределах блока, в котором они определены, и автоматически собираются в мусор, когда выходят из области видимости.

{
let myData = {
largeArray: new Array(1000000).fill("some data"),
id: 1
};

// действия с myData
// ...
}

Лучшие практики ручного управления памятью

JavaScript предоставляет инструменты и методы управления памятью, которые помогут контролировать использование памяти приложением.

1. Слабые ссылки

Одними из самых эффективных инструментов управления памятью в JavaScript являются WeakMap and WeakSet. Это специальные структуры данных, которые позволяют создавать слабые ссылки на объекты и переменные. Слабые ссылки отличаются от обычных тем, что они не мешают сборщику мусора освобождать память, используемую объектами. Поэтому они представляют собой отличные инструменты для предотвращения утечек памяти, вызванных циркулярными ссылками. Пример:

let object1 = {};
let object2 = {};

// создание WeakMap
let weakMap = new WeakMap();

// создание циркулярной ссылки путем добавления object1 в WeakMap
// и дальнейшего добавления WeakMap в object1
weakMap.set(object1, "some data");
object1.weakMap = weakMap;

// создание WeakSet и добавление туда object2
let weakSet = new WeakSet();
weakSet.add(object2);

// в этом случае сборщик мусора сможет освободить память,
// используемую object1 и object2, так как ссылки на них слабые

В этом примере мы создаем два объекта, object1 и object2, и циркулярные ссылки между ними, добавляя их в WeakMap и WeakSet соответственно. Поскольку ссылки на эти объекты являются слабыми, сборщик мусора сможет освободить используемую ими память несмотря на то, что на них все еще идут ссылки. Этот прием помогает предотвратить утечки памяти, вызванные циркулярными ссылками.

2. API сборщика мусора

Еще одна техника управления памятью заключается в использовании API сборщика мусора, который позволяет вручную запускать сборку мусора и получать информацию о текущем состоянии кучи. Этот прием полезен при отладке утечек памяти и проблем с производительностью. Пример:

let object1 = {};
let object2 = {};

// создание циркулярной ссылки между object1 и object2
object1.next = object2;
object2.prev = object1;

// ручной запуск сборки мусора
gc();

В этом примере мы создаем два объекта, object1 и object2, и циркулярную ссылку между ними, добавляя к ним свойства next и prev. Затем используем функцию gc(), чтобы вручную запустить сборку мусора. Это поможет освободить память, используемую объектами несмотря на то, что на них все еще ведут ссылки.

Важно отметить, что функция gc() поддерживается не всеми движками JavaScript, и ее поведение может отличаться в зависимости от того движка, который вы используете. Также следует помнить, что ручной запуск сборки мусора может повлиять на производительность, поэтому рекомендуется использовать его нечасто и только в случае необходимости.

Помимо gc(), JavaScript также предоставляет функцию global.gc() для конкретных движков JavaScript, а также performance.gc() под определенные движки браузера. Эти функции можно использовать для проверки текущего состояния кучи и измерения производительности процесса сборки мусора.

3. Снапшот кучи и профилировщики

JavaScript также предоставляет снапшот кучи и профилировщики. С помощью этих функций можно получить представление о том, как приложение использует память. Анализ моментального снимка кучи позволяет увидеть, какие объекты используют больше всего памяти.

Пример использования снапшота кучи для выявления утечек памяти в приложении:

// Запуск снапшота кучи
let snapshot1 = performance.heapSnapshot();

// Выполнение действий, которые могут привести к утечке памяти
for (let i = 0; i < 100000; i++) {
myArray.push({
largeData: new Array(1000000).fill("some data"),
id: i
});
}

// Выполнение еще одного снапшота кучи
let snapshot2 = performance.heapSnapshot();

// Сравнение двух снапшотов для определения созданных объектов
let diff = snapshot2.compare(snapshot1);

// Анализ различий для определения того, какие объекты используют больше всего памяти
diff.forEach(function(item) {
if (item.size > 1000000) {
console.log(item.name);
}
});

В этом примере мы делаем два снапшота кучи до и после выполнения цикла, который выталкивает большие данные в массив. Затем сравниваем эти два снапшота, чтобы определить объекты, которые были созданы во время цикла. После этого мы анализируем различия, чтобы увидеть, какие объекты используют больше всего памяти. Это позволит выявить утечки памяти, вызванные большими данными.

Профилировщики помогают отслеживать производительность приложения и определять области, где используется большое количество памяти:

let profiler = new Profiler();

profiler.start();

// выполнение действий, которые могут привести к утечке памяти
for (let i = 0; i < 100000; i++) {
myArray.push({
largeData: new Array(1000000).fill("some data"),
id: i
});
}
profiler.stop();
let report = profiler.report();

// анализ отчета для выявления областей с интенсивным использованием памяти
for (let func of report) {
if (func.memory > 1000000) {
console.log(func.name);
}
}

В этом примере мы используем профилировщик JavaScript для запуска и остановки отслеживания производительности приложения. В отчете будет указана информация о функциях, которые были вызваны, и об использовании памяти каждой из них.

Снапшоты кучи и профилировщики поддерживаются не всеми движками JavaScript и браузерами, поэтому важно проверять их совместимость, прежде чем использовать в приложении.


Заключение

Мы рассмотрели основы управления памятью в JavaScript, поговорили о процессе сборки мусора, различных типах памяти, а также обсудили инструменты и методы управления памятью, доступные в JavaScript. Мы также уделили внимание распространенным причинам утечек памяти и привели примеры того, как их избежать. Уделив время изучению и внедрению этих методов, вы сможете создавать приложения, которые исключают вероятность утечки памяти.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Vitalii Shevchuk: 🔥 JavaScript Memory Management: How to Avoid Common Memory Leaks and Improve Performance

Предыдущая статьяМасштабирование фронтенд-приложений в 2023 году
Следующая статьяРазделение окон в Vim