8 трюков для ускорения JavaScript

Парни из ag-Grid делятся своим опытом по оптимизации JavaScript в браузере.
11 октября 2017225057Андрей Никифоров1473012

Здравствуйте!

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

 

ag-Grid — библиотека для отображения больших объемов данных в браузере, внешне похожая на Excel и написанная на JS. Она хорошо работает даже в IE и на больших объемах данных. Сегодня мы расскажем о том, какие оптимизации мы используем, чтобы все работало быстро.

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

Виртуализация строк

Виртуализация строк означает то, что мы отрисовываем только те строки, которые видны на экране. Например, есть таблица на 10 тысяч строк, но только 40 из них на самом деле отрисовываются(каждая строка представлена одним DIV). По мере прокрутки библиотека на лету создает новые элементы для появляющихся строк.

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

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

Виртуализация колонок

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

Виртуализация колонок позволяет отображать большое количество колонок без деградации производительности.

Распространение событий

Проблема: регистрация обработчиков событий

Библиотека обрабатывает клики и нажатия клавиш для каждой ячейки, так что для каждой ячейки мы вынуждены регистрировать обработчики событий. Всего мы используем восемь событий: click, dblclick, mousedown, contextmenu, mouseover, mouseout, mouseenter and mouseleave.

Добавление обработчиков событий в DOM несколько влияет на производительность. Для таблицы в 20 видимых колонок и 50 видимых строк количество обработчиков равно 20 колонок * 50 строк * 8 событий = 8000 обработчиков событий. По мере прокрутки начинает работать виртуализация, и обработчики постоянно снимаются и регистрируются, создавая тормоза при прокрутке.

Решение: распространение событий

Шесть из восьми событий распространяемы, кроме mouseenter and mouseleave.  Так что вместо добавления обработчиков на каждую ячейку мы добавляем обработчик для контейнера, содержащего все ячейки. Таким образом мы добавляем обработчик при инициализации таблицы, а не индивидуально для каждой ячейки.

Вопрос в том, как определить, для какой ячейки вызвано событие?

// parentContainer will contain all the cells
var parentContainer = document.createElement('div');

// cells are regular DOM div objects
var eCell = document.createElement('div');
// attach our own properties to the DOM object.
// once we are not using DOM properties, there is no performance hit.
eCell.__col = colId;
eCell.__row = rowId;
// add the cell to the container, no listeners
parentContainer.appendChild(eCell);

// listen for clicks on the parent container only
parentContainer.addEventListener('click', myEventListener);

function myEventListener(event) {
   // go through all elements of the event, starting from the element that caused the event
   // and continue up to the parent container. we should find the cell element along the way.
   var domElement = event.target;
   var col, row;
   while (domElement!=parentContainer) {
       // see if the dom element has col and row info
       if (domElement.__col && domElement.__row) {
           // if yes, we have found the cell, and know which row and col it is
           col = domElement.__col;
           row = domElement.__row;
           break;
       }
       domElement = domElement.parentElement;
   }
}

Вероятно, вы заметили, что мы добавляем атрибуты __col и __row к элементам и думаете, безопасно ли это? Я надеюсь, что да, так как ag-Grid используется в приложении контроля авиатрафика над Австралией, насколько мне известно. Иными словами, библиотека работает так уже давно, и таким образом протестирована в боевой обстановке.

Это похоже на то, как работает React’s Synthetic Events. React использует делегирование событий и обрабатывает события в корне приложения, при этом отслеживая, какие ноды имеют обработчики. В React реализована собственная система событий, которая в итоге вызывает нужные обработчики.

Отбросьте DOM

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

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

В ag-Grid по мере создания строк мы используем композицию, чтобы создать сложную структуру в DOM. Однако удаляя строки, мы не удаляем ячейки вручную, вместо этого мы удаляем строку одним вызовом.

Используйте innerHTML, где это возможно

Какой самый быстрый способ добавить кучу ячеек и строк в браузер? Стоит ли использовать JavaScript(document.createElement()) для создания каждого элемента, обновления атрибутов и сборки элементов воедино с помощью appendChild()? Или вам стоит работать с фрагментами документа? Или может лучше собрать все в большой кусок HTML и вставить в DOM с помощью innerHTML?

Мы проверили все это. Ответ: используйте innerHTML.

Так что мы выигрываем в скорости с innertHTML, собирая большой HTML и вставляя его в DOM  с помощью element.insertAdjacentHTML(). Мы обнаружили, что insertAdjacentHTML() добавляет контент, в то время как innerHTML() заменяет его.

// build up the row's HTML in a string
var htmlParts = [];
htmlParts.push('<div class="ag-row">');
cells.forEach( function(cell) {
   htmlParts.push('<div class="ag-cell">');
   htmlParts.push(cell.getValue());
   htmlParts.push('</div>');
});
htmlParts.push('</div>');

// append the string into the DOM, one DOM hit for the entire row
var rowHtml = htmlParts.join('');
eContainer.insertAdjacentHTML(rowHtml);

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

Рендеры ячеек — отдельный тип компонентов. Сама идея компонентов хороша для приложений, они как строительные блоки для больших приложений, когда мелкие элементы (компоненты) собираются воедино в большие элементы. Однако в ag-Grid мы делаем ставку на скорость, так что лучше избегать рендеров ячеек в пользу быстрого рендеринга строк с innerHTML.

Если вы используете ag-Grid, вам может быть интересно, как использование рендеров ячеек влияет на производительность. Во многом это зависит от платформы: в Chrome рендеринг небольшой и несложной таблицы не создаст проблем. Однако при выводе больших таблиц в IE вы можете ощутить заметное падение производительности при использовании рендеров ячеек.

Объединение событий прокрутки

Когда вы прокручиваете таблицу в ag-Grid, виртуализация влечет за собой удаление элементов DOM. Удаление занимает время, и если оно вызывается внутри обработчика события, это пагубно влияет на ощущения от прокрутки.

Чтобы избежать этого, библиотека объединяет несколько близких событий прокрутки с кадрами анимации. Это привычный трюк для достижения плавной прокрутки и он хорошо объясняется в статье Leaner, Meaner, Faster Animations with RequestAnimationFrame. Я не буду повторно объяснять его принцип тут. Достаточно сказать, что мы обнаружили, что этот трюк хорошо поднимает производительность.

Кадры анимации

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

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

  • 1 задача: прокрутка к закрепленной панели, если пользователь ее закрепляет
  • n задач: вставить контейнеры со строками (в результате строки раскрашены в нужные цвета)
  • n задач: вставить ячейки, используя innertHTML
  • n задач: вставить ячейки, используя рендер ячеек
  • n задач: вставить обработчики mouseenter and mouseleave на каждую строку для добавления hover-эффекта
  • n задач: удалить старые строки

Так что если вы прокрутили таблицу, чтобы увидеть 10 новых строк, у вас появляется 50 с лишним задач. Каждая прокрутка, создание строки, создание ячеек — все это отдельные задачи.

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

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

Учитывая все вышесказанное, такое количество задач потребует много кадров. Чтобы избежать этого, библиотека не выполняет каждую задачу в отдельном кадре, особенно в связи с тем, что само создание, удаление и планирование кадров требует ресурсов. Вместо этого библиотека запрашивает кадр и выполняет столько задач, сколько сможет, в течении 60 миллисекунд. Мы выбрали такой промежуток после тестов. Если библиотека не успеет выполнить все задачи, она запросит новый кадр и будет выполнять оставшиеся задачи в нем, пока они не кончатся.

Быстрые браузеры вроде Chrome могут сделать все необходимое в одном кадре без видимой визуальной задержки. Медленным браузерам вроде IE может потребоваться 10 и более кадров для выполнения всех задач. Внешне это выглядит, как поступенчатая отрисовка, что гораздо лучше блокирования UI и отрисовки всего в один момент.

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

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

Упорядочивание строк

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

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

Однако компромисс возможен. Обычно библиотека не сортирует строки. Если пользователь хочет поддерживать порядок строк, он может использовать свойство ensureDomOrder=true. Таблица отображается немного медленнее, но тогда она совместима с программами для чтения с экрана.

Оригинал: ag-grid.com/ag-grid-8-performance-hacks-for-javascript