Утечки памяти в IE8, или страшная сказка со счастливым концом

в 16:00, , рубрики: ie8, javascript, утечки памяти, метки: , ,

Утечки памяти в IE8, или страшная сказка со счастливым концом

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

Однажды в одном большом-большом городе, в одной большой-большой ИТ-компании тестировали один большой-большой проект в одном очень используемом браузере. И обнаружили там утечки памяти. Большие-большие. Прям незадолго до релиза.

И было бы это неудивительно, если бы разработчики были совсем глупые. Но нет же, разработчики наизусть знали «Understanding and Solving Internet Explorer Leak Patterns». Циклические ссылки разрывали, замыкания не использовали, к событиям относились с должным почтением и удалять обработчики не забывали. Да вот только от утечек это не спасло.

Discalimer: все упомянутые ниже сущности, события и фрагменты кода являются художественным вымыслом. Все совпадения случайны.

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

var cell = tableEl.firstChild.rows[0].cells[0];

И пришли разработчики в недоумение, пришлось им заняться исследованием проблемы и её решением. С чем они и управились.

А дальше сказка кончается и начинается код демонстрирующий проблему:

<!DOCTYPE HTML>
<html>
<body>
<span id="count">0</span> <input id="num" value="1000" />
<input type="button" value="GO!" onclick="execute()" />
<hr />
<div id="test"></div>
</body>

<script type="text/javascript">
var count = 0;
function execute()
{
var val = document.getElementById('num').value;
for (var i = 0; i < val; i++)
{
var domEl = document.getElementById('test');
domEl.innerHTML = '<table><tbody><tr><td>A1</td></tr><tr><td>B1</td></tr></tbody></table>';
domEl.firstChild.insertRow(0);
domEl.removeChild(domEl.firstChild);
count++;
}
document.getElementById('count').innerHTML = count;
};
</script>
</html>

Запускаем Process Explorer и несколько раз нажимаем «GO!» — убегает по 10 мегабайт с нажатия:

image

Утечка скрывается в строчке domEl.firstChild.rows[0] в чем можно убедиться, просто её закомментировав. Причем проявляется баг только в режиме IE8 Standart, режимы IE7 и IE8 Quirks данному заболеванию не подвержены.

Замена на domEl.firstChild.firstChild или на getElementById помогает устранить утечку, но если таких строчек сотни по всему проекту? Может быть, есть менее трудоемкий вариант?

Попробуем прикинуть, как индусы из майкрософт сумели достичь такого эффекта. Очевидно, коллекции столбцов и ячеек создаются только когда используются, решение вполне здравое. А когда таблицу отцепляют от DOM дерева документа, их не уничтожают — тоже вполне понятно и разумно. Только вот в сборщике мусора почему-то забыли учесть образующиеся при этом циклические ссылки.
Гипотезу можно проверить. Если дело в создании коллекций, утечки будут и в других случаях, когда используются индексы. Заменим rows[0] на insertRow(0). Бинго! Утечка сохранилась.

Теперь ясен и способ борьбы. Раз дело в циклических ссылках между DOM элементами TBODY и TD, значит, нужно просто удалить всех потомков TBODY:

while(domEl.firstChild.firstChild) {
while(domEl.firstChild.firstChild.firstChild)
domEl.firstChild.firstChild.removeChild(domEl.firstChild.firstChild.firstChild);
domEl.firstChild.removeChild(domEl.firstChild.firstChild);
}

Сработало, но этого всё ещё недостаточно для счастья, заботиться отдельно о правильном удалении таблицы в компоненте достаточно муторно, да и забыть что-нибудь недолго. Можно, конечно, автоматизировать процесс, использовать что-то вроде getElementsByTag('TABLE')… Но не нужно, так как есть волшебное свойство innerHTML, которое все сделает за нас:

Заменяем domEl.removeChild(domEl.firstChild) на domEl.innerHTML = ''
Проверяем. Работает.

Подводим итог:

Данный тип утечки не документирован, возникает только в режиме IE8: Standart при использовании методов и свойств объектов DOM таблицы, использующих индексы строк или столбцов. (.rows[], .cells[], insertRow(), и т.д.)

Лучший вариант обхода проблемы — не использовать данные методы и свойства.

Альтернативный — следить за тем, чтобы DOM элементы всех строк таблицы были отсоединены от своего предка, либо использовать самый простой вариант: innerHTML = ''

Автор: Aniro

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js