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

Распространение SPA (одностраничных приложений) побуждает нас уделять больше внимания методам кодирования JavaScript, связанным с памятью. Поскольку приложение потребляет все больше и больше памяти, оно может серьезно снизить производительность и даже привести к зависанию вкладки браузера.

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

Читайте также: Лучшие вопросы для собеседования по JavaScript

Что такое утечка памяти и как ее обнаружить?

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

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

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

Диспетчер задач (не путать с диспетчером задач операционной системы). Они дают нам обзор всех вкладок и процессов, запущенных в браузере. Доступ к диспетчеру можно получить в Chrome, нажав Shift+Esc в Linux и Windows, в то время как менеджер, встроенный в Firefox, набрав about:performance в адресной строке, позволяет нам, среди прочего, увидеть объем памяти JavaScript для каждой вкладки. наш сайт сидит там и ничего не делает, но использование памяти javascript постепенно увеличивается, у нас, скорее всего, есть утечка памяти.

Инструменты разработчика предлагают расширенные методы управления памятью. Запись в инструменте производительности Chrome позволяет нам визуально анализировать производительность страницы во время ее работы. Некоторые шаблоны типичны для утечек памяти, например шаблон увеличенного использования памяти кучи, показанный ниже.

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

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

Исследование причин утечек памяти на самом деле является поиском шаблонов программирования, которые могут заставить нас сохранять ссылки на объекты, которые в противном случае подлежали бы сборке мусора. Ниже приведен полезный список мест в вашем коде, которые более подвержены утечкам памяти и заслуживают особое внимание при управлении памятью.1. Случайные глобальные переменные.

1. Случайные глобальные переменные

Глобальные переменные всегда доступны в корневом каталоге и никогда не извлекаются. Некоторые ошибки вызывают миграцию переменных из локальной области в глобальную, когда они не в режиме:

  • Присвоить значение необъявленной переменной,
  • с «this», указывающим на глобальный объект.
function createGlobalVariables() {
  leaking1 = 'I leak into the global scope'; // assigning value to the undeclared variable
  this.leaking2 = 'I also leak into the global scope'; // 'this' points to the global object
};
createGlobalVariables();
window.leaking1; // 'I leak into the global scope'
window.leaking2; // 'I also leak into the global scope'

Избегать. Строгий режим («использовать строгий») поможет вам избежать утечек памяти и ошибок консоли в приведенном выше примере.

2. Закрытие

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

function outer() {
  const potentiallyHugeArray = [];

  return function inner() {
    potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable
    console.log('Hello');
  };
};
const sayHello = outer(); // contains definition of the function inner

function repeat(fn, num) {
  for (let i = 0; i < num; i++){
    fn();
  }
}
repeat(sayHello, 10); // each sayHello call pushes another 'Hello' to the potentiallyHugeArray

// now imagine repeat(sayHello, 100000)

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

Как этого избежать. замыкания неизбежны и являются частью JavaScript, поэтому важно:

  • понять, когда было создано замыкание и какие объекты оно содержит,
  • Поймите ожидаемое время жизни и использование замыкания (особенно когда оно используется в качестве обратного вызова).

3. Таймеры

Имейте setTimeout или setInterval Ссылка на объект в обратном вызове является наиболее распространенным способом предотвращения захвата объектов. Когда мы устанавливаем повторяющийся таймер в нашем коде (мы можем setTimeout вести себя как setInterval, то есть сделать его рекурсивным), ссылка на объект обратного вызова таймера остается активной до тех пор, пока обратный вызов доступен.

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

function setCallback() {
  const data = {
    counter: 0,
    hugeString: new Array(100000).join('x')
  };

  return function cb() {
    data.counter++; // data object is now part of the callback's scope
    console.log(data.counter);
  }
}

setInterval(setCallback(), 1000); // how do we stop it?

Как этого избежать, особенно если продолжительность обратного вызова неопределенная или неопределенная:

  • Остерегайтесь объектов, на которые ссылается обратный вызов таймера.
  • Используйте идентификатор, возвращенный таймером, чтобы отменить его при необходимости.
function setCallback() {
  // 'unpacking' the data object
  let counter = 0;
  const hugeString = new Array(100000).join('x'); // gets removed when the setCallback returns

  return function cb() {
    counter++; // only counter is part of the callback's scope
    console.log(counter);
  }
}

const timerId = setInterval(setCallback(), 1000); // saving the interval ID

// doing something ...

clearInterval(timerId); // stopping the timer i.e. if button pressed

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

Активный прослушиватель событий предотвращает сбор всех переменных, собранных в его области. После добавления прослушиватель событий остается в силе до тех пор, пока:

  • явно удалено с помощью removeEventListener()
  • связанный элемент DOM удаляется.

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

const hugeString = new Array(100000).join('x');

document.addEventListener('keyup', function() { // anonymous inline function - can't remove it
  doSomething(hugeString); // hugeString is now forever kept in the callback's scope
});

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

Как этого избежать. Нам нужно остановить запись события. Освободите прослушиватель, когда в нем больше нет необходимости ссылаться на него, и передайте его removeEventListener().

function listener() {
  doSomething(hugeString);
}

document.addEventListener('keyup', listener); // named function can be referenced here...
document.removeEventListener('keyup', listener); // ...and here

В случае, если прослушиватель событий нужно запустить только один раз, addEventListener() может принимать третий параметр, объект, предоставляющий дополнительные параметры. При условии, что {once: true} передается в качестве третьего параметра в addEventListener(), слушатель будет автоматически удален, как только событие будет обработано один раз.

document.addEventListener('keyup', function listener() {
  doSomething(hugeString);
}, {once: true}); // listener will be removed after running once

5. Кэш

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

let user_1 = { name: "Peter", id: 12345 };
let user_2 = { name: "Mark", id: 54321 };
const mapCache = new Map();

function cache(obj){
  if (!mapCache.has(obj)){
    const value = `${obj.name} has an id of ${obj.id}`;
    mapCache.set(obj, value);

    return [value, 'computed'];
  }

  return [mapCache.get(obj), 'cached'];
}

cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_1); // ['Peter has an id of 12345', 'cached']
cache(user_2); // ['Mark has an id of 54321', 'computed']

console.log(mapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321")
user_1 = null; // removing the inactive user

// Garbage Collector
console.log(mapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321") // first entry is still in cache

пользователь_1. Следовательно, нам также необходимо очистить кеш записей, которые никогда не будут использоваться повторно.

Возможное решение: Чтобы решить эту проблему, мы можем использовать Weak Map . Это структура данных со слабыми ссылками на ключи, принимающая в качестве ключей только объекты. Если мы используем объект в качестве ключа, и это единственная ссылка на этот объект, связанная запись удаляется из кеша и собирается мусор. В следующем примере после замены user_1 связанная запись будет автоматически удалена из WeakMap после следующий сборщик мусора.

let user_1 = { name: "Peter", id: 12345 };
let user_2 = { name: "Mark", id: 54321 };
const weakMapCache = new WeakMap();

function cache(obj){
  // ...same as above, but with weakMapCache

  return [weakMapCache.get(obj), 'cached'];
}

cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_2); // ['Mark has an id of 54321', 'computed']
console.log(weakMapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321"}
user_1 = null; // removing the inactive user

// Garbage Collector

console.log(weakMapCache); // ((…) => "Mark has an id of 54321") - first entry gets garbage collected

Заключение

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