Опубликован: 16.02.2009 | Уровень: специалист | Доступ: платный
Лекция 7:

Оптимизация JavaScript

Постраничные утечки

Утечки, которые зависят от порядка добавления элементов в DOM-дереве, всегда вызваны тем, что создаются промежуточные объекты, которые затем не удаляются должным образом. Это происходит и в случае создания динамических элементов, которые затем присоединяются к DOM. Базовый шаблон заключается во временном соединении двух только что созданных элементов, из-за чего возникает область видимости, в которой определен дочерний элемент. Затем, при включении этого дерева из двух элементов в основное, они оба наследуют контекст всего документа, и происходит утечка во временном объекте (чей контекст не был закрыт).

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

 Утечки, связанные с порядком добавления DOM-элементов

Рис. 7.5. Утечки, связанные с порядком добавления DOM-элементов

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

Запуская пример и возвращаясь к пустой странице, можно замерить разницу в объеме памяти между этими двумя случаями. При использовании первой DOM-модели для прикрепления дочернего узла к родительскому, а затем родительского - к общему дереву, нагрузка на память немного возрастает. Данная утечка использования перекрестных ссылок характерна для Internet Explorer. При этом память не высвобождается, если мы перезапустим IE-процесс. Если протестировать пример, используя вторую DOM-модель для тех же самых действий, то никакого изменения в размере памяти не последует. Таким образом, можно исправить утечки такого рода:

<script type="text/javascript">
	
	function LeakMemory()
	{
		var hostElement = document.getElementById("hostElement");

	// Давайте посмотрим, что происходит с памятью в Диспетчере Задач

		for (i = 0; i < 5000; i++)
		{
			var parentDiv = document.
				createElement("<div onClick='foo()'>");
			var childDiv = document.
				createElement("<div onClick='foo()'>");

		// Здесь вызывается утечка на временном объекте
			parentDiv.appendChild(childDiv);
			hostElement.appendChild(parentDiv);
			hostElement.removeChild(parentDiv);
			parentDiv.removeChild(childDiv);
			parentDiv = null;
			childDiv = null;
		}
	  	hostElement = null;
	}

	function CleanMemory()
	{
		var hostElement = document.getElementById("hostElement");

	// Опять смотрим в Диспетчере Задач на использование памяти

		for(i = 0; i < 5000; i++)
		{
			var parentDiv = document.
				createElement("<div onClick='foo()'>");
			var childDiv = document.
				createElement( "<div onClick='foo()'>");

	// Изменение порядка имеет значение. Теперь утечек нет
			hostElement.appendChild(parentDiv);
			parentDiv.appendChild(childDiv);
			hostElement.removeChild(parentDiv);
			parentDiv.removeChild(childDiv);
			parentDiv = null;
			childDiv = null;
		}
		hostElement = null;
	}

</script>

<button onclick="LeakMemory()">Вставить с утечками памяти</button>
<button onclick="CleanMemory()">Вставить без утечек</button>

<div id="hostElement"></div>

Стоит немного прокомментировать приведенный пример, потому что он противоречит некоторым практическим советам, которые дают относительно написания скриптов для IE. Ключевым моментом в данном случае для осознания причины утечки является то, что DOM-элементы создаются с прикрепленными к ним обработчиками событий. Это является критичным для утечки, потому что в случае обычных DOM-элементов, которые не содержат никаких скриптов, их можно присоединять друг к другу в обычном режиме, не опасаясь проблем, связанных с утечками.

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

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

Псевдо-утечки

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

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

<script type="text/javascript">

	function LeakMemory()
	{
	// Посмотрим, что происходит с памятью в Диспетчере Задач
	  
		for(i = 0; i < 5000; i++)
		{
			hostElement.text = "function foo() { }";
		}
	}

</script>

<button onclick="LeakMemory()">Вставить с утечками памяти</button>
<script id="hostElement">function foo() { }</script>

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

Проектируем утечки

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

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

Оптимизируем "тяжелые" JavaScript-вычисления

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

К тому же в веб-браузерах у JavaScript-процесса имеется ограниченное время для завершения своего выполнения (это может быть фиксированное число в случае браузеров на движке Mozilla или какое-либо другое ограничение, например максимальное число элементарных операций в случае Internet Explorer). Если скрипт выполняется слишком долго, то пользователю выводится диалоговое окно, в котором запрашивается, нужно ли прервать скрипт.

Дарья Билялова
Дарья Билялова
Россия
Елена Петрушевская
Елена Петрушевская
Россия, г. Нижневартовск