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

Повышение количества SPA (Single Page Application - одностраничное приложение) побуждает нас уделять больше внимания хорошим методам кодирования, связанным с памятью. Использование SPA подразумевает нахождение на одной странице в течение гораздо более длительного времени. Если страница, которая никогда не перезагружается полностью, начинает постепенно использовать все больше и больше памяти, это может серьезно повлиять на производительность и даже вызвать сбой вкладки браузера.

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

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

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

Пример цепочки ссылок от корня сборщика мусора к объектам

Пример цепочки ссылок от корня сборщика мусора к объектам

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

Недоступность объекта

Объект 4 недоступен и будет удален из памяти. Объект 3 по-прежнему доступен через забытую ссылку из объекта 2 и не будет собирать мусор.

Как выяснить, что в нашем коде есть утечки памяти?

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

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

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

Инструменты разработчика (Developer Tools) предоставляют более продвинутые методы управления памятью. С помощью инструмента производительности Chrome мы можем визуально анализировать производительность страницы во время ее работы. Некоторые шаблоны типичны для утечек памяти, например, схема увеличения использования динамической памяти, показанная ниже.

потребление памяти в Chrome

Производительность записи в Chrome - потребление памяти постоянно растет (синяя линия)

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

Общие источники утечек памяти в коде JavaScript

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

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

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

  • присвоение значения необъявленной переменной,
  • использование ключевого слова this, которое указывает на глобальный объект.

Как это предотвратить: Строгий режим ("use strict") предотвратит случайные утечки, так как код из примера выдаст ошибку.

2. Замыкания

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

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

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

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

3. Таймеры

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

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

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

  • быть осведомленным об объектах, на которые ссылается обратный вызов таймера,
  • используя дескриптор, связанный с таймера, отменить вызов таймера (интервала) методом clearTimer()  (clearInterval()) при необходимости.

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

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

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

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

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

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

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

Немного изменим скрипт, чтобы вы могли попробовать сами:

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

5. Кеш

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

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

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

6. Отдельные элементы DOM

Если узел DOM имеет прямые ссылки из JavaScript, это предотвратит сбор мусора даже после удаления узла из дерева DOM.

В следующем примере мы создали элемент div и добавили его в document.body. Метод removeChild() не работает должным образом, и снимок кучи покажет отдельный HTMLDivElement, так как есть переменная, все еще указывающая на div.

Как это предотвратить? Одним из возможных решений является перемещение ссылок DOM в локальную область функции. В приведенном ниже примере переменная, указывающая на элемент DOM, удаляется после завершения функции appendElement().

Вывод

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

Источник: Causes of Memory Leaks in JavaScript and How to Avoid Them

 

Автор: Админ

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *