Автор оригинальной стать: Vitalii Shevchuk

Мы объясним управление памятью в JS, которое поможет вам оптимизировать ваши приложения.

Вступление

Как веб-разработчик, вы знаете, что каждая строка кода, которую вы пишете, может повлиять на производительность вашего приложения. И когда дело доходит до JavaScript, одной из наиболее важных областей, на которой следует сосредоточиться, является управление памятью.
Подумайте об этом, каждый раз, когда пользователь взаимодействует с вашим сайтом, он создает новые объекты, переменные и функции. И если вы не будете осторожны, эти объекты могут накапливаться, засоряя память браузера и замедляя работу и негативно влияя на пользовательский опыт. Это похоже на пробку на информационной супермагистрали — узкое место, которое может привести к потери аудитории пользователей.
Но не обязательно все должно быть так плохо. Обладая правильными знаниями и техниками, вы можете взять под контроль свою память JavaScript и обеспечить бесперебойную и эффективную работу ваших приложений. В этой статье мы рассмотрим все тонкости управления памятью JavaScript, включая распространенные причины утечек памяти и стратегии их предотвращения. Являетесь ли вы профессионалом или начинающим разработчиком js, вы уйдете с более глубоким пониманием того, как писать компактный и быстрый код.

Понимание управления памятью JavaScript

Сборчик мусора

Движок JavaScript использует сборщик мусора (garbage collector) для освобождения памяти, которая больше не используется. Задача сборщика мусора заключается в выявлении и удалении объектов, которые больше не используются вашим приложением. Он делает это, постоянно отслеживая объекты и переменные в вашем коде и отслеживая, на какие из них все еще ссылаются.
Сборщик мусора использует для управления памятью технику под названием «пометить и очистить»(mark and sweep). Он начинает с маркировки всех объектов, которые все еще используются, затем «просматривает» кучу и удаляет все объекты, которые не отмечены. Этот процесс выполняется периодически, а также когда в куче не хватает памяти, чтобы приложение всегда использовало память максимально эффективно.

Стек и куча

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

Распространенные причины утечек памяти

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

Цикличная ссылка

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

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

// создаем циклические ссылки между объектами object1 и object2
object1.next = object2;
object2.prev = object1;

// работаем с объектами object1 и object2
// ...

// присваиваем null объектам object1 и object2 для избавления от циклической ссылки
object1 = null;
object2 = null;

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

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

delete object1.next;
delete object2.prev;

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

Обработчики событий

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

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

// добавляем обработчик события для button
button.addEventListener("click", function () {
  console.log("Button was clicked!");
});

// работаем с button
// ...

// удаляем button из DOM
button.parentNode.removeChild(button);

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

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

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

Другой способ - использовать метод EventTarget.removeAllListeners(), который удаляет все прослушиватели событий, которые были добавлены к определенному целевому объекту события.

button.removeAllListeners();

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

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

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

// do something with myData
// ...

// set myData to null to break the reference
myData = null;

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

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

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

  // do something with myData
  // ...
}
myFunction();

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

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

  // do something with myData
  // ...
}

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

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

Использование слабых ссылок

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

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

// create a WeakMap
let weakMap = new WeakMap();

// create a circular reference by adding object1 to the WeakMap
// and then adding the WeakMap to object1
weakMap.set(object1, "some data");
object1.weakMap = weakMap;

// create a WeakSet and add object2 to it
let weakSet = new WeakSet();
weakSet.add(object2);

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

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

Использолвание API сборщика мусора

Другим методом управления памятью является использование API garbage collector, который позволяет вручную запускать сборку мусора и получать информацию о текущем состоянии кучи. Это может быть полезно для отладки утечек памяти и проблем с производительностью. Вот пример:

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

// create a circular reference between object1 and object2
object1.next = object2;
object2.prev = object1;

// manually trigger garbage collection
gc();

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

Использование снимков кучи и профилировщиков

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

// Start a heap snapshot
let snapshot1 = performance.heapSnapshot();

// Do some actions that might cause memory leaks
for (let i = 0; i < 100000; i++) {
  myArray.push({
    largeData: new Array(1000000).fill("some data"),
    id: i,
  });
}

// Take another heap snapshot
let snapshot2 = performance.heapSnapshot();

// Compare the two snapshots to see which objects were created
let diff = snapshot2.compare(snapshot1);

// Analyze the diff to see which objects are using the most memory
diff.forEach(function (item) {
  if (item.size > 1000000) {
    console.log(item.name);
  }
});

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

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

let profiler = new Profiler();

profiler.start();

// do some actions that might cause memory leaks
for (let i = 0; i < 100000; i++) {
  myArray.push({
    largeData: new Array(1000000).fill("some data"),
    id: i,
  });
}

profiler.stop();

let report = profiler.report();

// analyze the report to identify areas where memory usage is high
for (let func of report) {
  if (func.memory > 1000000) {
    console.log(func.name);
  }
}

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

Заключение

Мы рассмотрели основы управления памятью JavaScript, включая процесс сборки мусора, различные типы памяти(стек и куча), а также инструменты и методы управления памятью, доступные в JavaScript. Мы также обсудили распространенные причины утечек памяти и привели примеры того, как их избежать.
Потратив время на понимание и внедрение этих рекомендаций по управлению памятью, вы сможете создавать приложения, исключающие вероятность утечек памяти. Если вы нашли эту статью полезной, пожалуйста, покажите свою поддержку, похлопав в ладоши ???? и подумайте о том, чтобы подписаться на меня на Medium, чтобы быть в курсе будущих статей и возможностей обучения, которые помогут вам поднять свои навыки на новый уровень.

Перевел для Вас mr_dramm ссылка на мой сайт

Комментарии (0)