Независимо от того, какой тип приложения вы разрабатываете, оптимизация его производительности, быстрая загрузка и плавное взаимодействие имеют решающее значение для удобства пользователя и успеха приложения. Один из способов сделать это — проверить активность приложения с помощью инструментов профилирования, чтобы увидеть, что происходит внутри приложения во время его работы в течение определенного временного окна. Панель «Производительность» в DevTools — отличный инструмент профилирования для анализа и оптимизации производительности веб-приложений. Если ваше приложение работает в Chrome, оно дает вам подробный визуальный обзор того, что делает браузер во время выполнения вашего приложения. Понимание этого действия может помочь вам выявить закономерности, узкие места и проблемные точки производительности, над которыми можно действовать, чтобы повысить производительность.
В следующем примере показано, как использовать панель «Производительность» .
Настройка и воссоздание нашего сценария профилирования
Недавно мы поставили перед собой цель сделать панель «Производительность» более производительной. В частности, мы хотели, чтобы он быстрее загружал большие объемы данных о производительности. Это имеет место, например, при профилировании длительных или сложных процессов или при сборе данных с высокой степенью детализации. Для этого сначала необходимо было понять, как работает приложение и почему оно работает именно так, что было достигнуто с помощью инструмента профилирования.
Как вы, возможно, знаете, DevTools сам по себе является веб-приложением. Таким образом, его можно профилировать с помощью панели «Производительность» . Чтобы профилировать эту панель, вы можете открыть DevTools, а затем открыть другой экземпляр DevTools, прикрепленный к ней. В Google эта настройка известна как DevTools-on-DevTools .
Когда настройка готова, необходимо воссоздать и записать сценарий, который будет профилироваться. Во избежание путаницы исходное окно DevTools будет называться « первым экземпляром DevTools», а окно, проверяющее первый экземпляр, будет называться « вторым экземпляром DevTools».

Во втором экземпляре DevTools панель «Производительность », которая в дальнейшем будет называться панелью производительности , наблюдает за первым экземпляром DevTools, чтобы воссоздать сценарий, который загружает профиль.
На втором экземпляре DevTools запускается живая запись, а на первом экземпляре профиль загружается из файла на диске. Загружается большой файл, чтобы точно профилировать производительность обработки больших входных данных. Когда оба экземпляра завершают загрузку, данные профилирования производительности, обычно называемые трассировкой , отображаются во втором экземпляре DevTools панели производительности, загружающей профиль.
Исходное состояние: выявление возможностей для улучшения
После завершения загрузки на следующем снимке экрана на нашем втором экземпляре панели производительности наблюдалось следующее. Сосредоточьтесь на активности основного потока, которая отображается под дорожкой с надписью Main . Можно увидеть, что в карте пламени есть пять больших групп активности. К ним относятся задачи, загрузка которых занимает больше всего времени. Общее время выполнения этих задач составило примерно 10 секунд . На следующем снимке экрана панель производительности используется для фокусировки на каждой из этих групп действий и просмотра того, что можно найти.

Первая группа действий: ненужная работа
Стало очевидно, что первая группа действий представляла собой устаревший код, который все еще работал, но в действительности не был нужен. По сути, все, что находится под зеленым блоком с надписью processThreadEvents
, было напрасной тратой усилий. Это была быстрая победа. Удаление этого вызова функции сэкономило около 1,5 секунд времени. Прохладный!
Вторая группа активности
Во второй группе действий решение было не таким простым, как в первой. Вызов buildProfileCalls
занял около 0,5 секунды, и этой задачи нельзя было избежать.

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

Чтобы проверить это подозрение, мы использовали для расследования панель «Память» (еще одна панель в DevTools, отличная от секции «Память» на панели производительности). На панели «Память» был выбран тип профилирования «Выборка распределения», при котором записывался снимок кучи для панели производительности, загружающей профиль ЦП.

На следующем снимке экрана показан собранный снимок кучи.

Из этого снимка кучи было замечено, что класс Set
потребляет много памяти. Проверив точки вызова, выяснилось, что мы без необходимости присваивали свойства типа Set
объектам, которые создавались в больших объемах. Эти затраты накапливались, и потреблялось много памяти до такой степени, что приложение часто аварийно завершало работу при больших входных данных.
Наборы полезны для хранения уникальных элементов и предоставляют операции, использующие уникальность их содержимого, например дедупликацию наборов данных и обеспечение более эффективного поиска. Однако в этих функциях не было необходимости, поскольку сохраняемые данные гарантированно были уникальными из источника. Таким образом, в наборах изначально не было необходимости. Чтобы улучшить распределение памяти, тип свойства был изменен с Set
на простой массив. После применения этого изменения был сделан еще один снимок кучи, и наблюдалось уменьшение выделения памяти. Несмотря на то, что это изменение не привело к значительному увеличению скорости, дополнительным преимуществом было то, что приложение реже зависало.

Третья группа действий: взвешивание компромиссов в структуре данных
Третий раздел своеобразен: на флейм-диаграмме видно, что он состоит из узких, но высоких столбцов, обозначающих глубокие вызовы функций и в данном случае глубокие рекурсии. В общей сложности этот раздел длился около 1,4 секунды. Глядя на нижнюю часть этого раздела, стало очевидно, что ширина этих столбцов определяется длительностью одной функции: appendEventAtLevel
, что предполагает, что это может быть узким местом.
В реализации функции appendEventAtLevel
выделяется одна вещь. Для каждой отдельной записи входных данных (которая в коде называется «событием») на карту добавлялся элемент, который отслеживал вертикальное положение записей временной шкалы. Это было проблематично, поскольку количество хранившихся предметов было очень большим. Карты обеспечивают быстрый поиск по ключам, но это преимущество не предоставляется бесплатно. По мере того, как карта становится больше, добавление к ней данных может, например, стать дорогостоящим из-за повторного хеширования. Эта стоимость становится заметной, когда на карту последовательно добавляется большое количество предметов.
/**
* Adds an event to the flame chart data at a defined vertical level.
*/
function appendEventAtLevel (event, level) {
// ...
const index = data.length;
data.push(event);
this.indexForEventMap.set(event, index);
// ...
}
Мы экспериментировали с другим подходом, который не требовал от нас добавления элемента на карту для каждой записи в диаграмме пламени. Улучшение было значительным, подтверждая, что узкое место действительно было связано с накладными расходами, возникающими при добавлении всех данных на карту. Время, которое потребовалось группе действий, сократилось примерно с 1,4 секунды до примерно 200 миллисекунд.
До:

После:

Четвертая группа действий: отсрочка некритической работы и кэширования данных для предотвращения дублирования работы.
Увеличив это окно, можно увидеть, что имеется два практически идентичных блока вызовов функций. Посмотрев на имена вызываемых функций, вы можете сделать вывод, что эти блоки состоят из кода, строящего деревья (например, с такими именами, как refreshTree
или buildChildren
). Фактически, соответствующий код создает древовидные представления в нижнем ящике панели. Что интересно, эти древовидные представления не отображаются сразу после загрузки. Вместо этого пользователю необходимо выбрать древовидное представление (вкладки «Снизу вверх», «Дерево вызовов» и «Журнал событий» в ящике) для отображения деревьев. Кроме того, как видно из скриншота, процесс построения дерева выполнялся дважды.

На этом изображении мы выявили две проблемы:
- Некритическая задача тормозила производительность времени загрузки. Пользователям не всегда нужны его результаты. Таким образом, задача не является критичной для загрузки профиля.
- Результат этих задач не кэшировался. Поэтому деревья были рассчитаны дважды, хотя данные не менялись.
Мы начали с того, что отложили вычисление дерева до момента, когда пользователь вручную открывал древовидное представление. Только тогда стоит заплатить цену за создание этих деревьев. Общее время выполнения этого дважды составило около 3,4 секунды, поэтому отсрочка существенно увеличила время загрузки. Мы все еще работаем над кэшированием задач такого типа.
Пятая группа действий: по возможности избегайте сложных иерархий вызовов.
Присмотревшись к этой группе, стало ясно, что определенная цепочка вызовов вызывалась неоднократно. Один и тот же паттерн появился 6 раз в разных местах диаграммы пламени, а общая продолжительность этого окна составила около 2,4 секунды!

Связанный код, вызываемый несколько раз, — это часть, которая обрабатывает данные, которые будут отображаться на «мини-карте» (обзор активности временной шкалы в верхней части панели). Непонятно, почему это происходило несколько раз, но уж точно не обязательно, чтобы это происходило 6 раз! Фактически, выходные данные кода должны оставаться текущими, если никакой другой профиль не загружен. Теоретически код должен запускаться только один раз.
В ходе расследования было обнаружено, что связанный код был вызван из-за того, что несколько частей в конвейере загрузки прямо или косвенно вызывали функцию, вычисляющую мини-карту. Это связано с тем, что сложность графа вызовов программы со временем менялась, и по незнанию к этому коду добавлялось больше зависимостей. Быстрого решения этой проблемы не существует. Способ решения зависит от архитектуры рассматриваемой кодовой базы. В нашем случае пришлось немного уменьшить сложность иерархии вызовов и добавить проверку, предотвращающую выполнение кода, если входные данные остались неизменными. После реализации мы получили такой взгляд на график:

Обратите внимание, что выполнение рендеринга мини-карты происходит дважды, а не один раз. Это связано с тем, что для каждого профиля рисуются две мини-карты: одна для обзора в верхней части панели, а другая для раскрывающегося меню, которое выбирает видимый в данный момент профиль из истории (каждый элемент в этом меню содержит обзор выбранного профиля). Тем не менее, эти два имеют одно и то же содержимое, поэтому один из них можно использовать повторно для другого.
Поскольку обе эти мини-карты представляют собой изображения, нарисованные на холсте, пришлось использовать утилиту холста drawImage
и последующий запуск кода только один раз, чтобы сэкономить дополнительное время. В результате этих усилий продолжительность группы сократилась с 2,4 секунды до 140 миллисекунд.
Заключение
После применения всех этих исправлений (и еще пары более мелких кое-где) изменение графика загрузки профиля выглядело следующим образом:
До:

После:

Время загрузки после улучшений составило 2 секунды, а это означает, что улучшение примерно на 80% было достигнуто с относительно небольшими усилиями, поскольку большая часть того, что было сделано, состояла из быстрых исправлений. Конечно, правильное определение того, что делать на начальном этапе, было ключевым моментом, и панель производительности стала для этого подходящим инструментом.
Также важно подчеркнуть, что эти цифры относятся к профилю, используемому в качестве объекта исследования. Профиль был нам интересен тем, что был особенно большим. Тем не менее, поскольку конвейер обработки одинаков для каждого профиля, достигнутое значительное улучшение применимо к каждому профилю, загруженному на панель производительности.
Вынос
Из этих результатов можно извлечь некоторые уроки с точки зрения оптимизации производительности вашего приложения:
1. Используйте инструменты профилирования для выявления закономерностей производительности во время выполнения.
Инструменты профилирования невероятно полезны для понимания того, что происходит в вашем приложении во время его работы, особенно для выявления возможностей повышения производительности. Панель «Производительность» в Chrome DevTools — отличный вариант для веб-приложений, поскольку это встроенный инструмент веб-профилирования в браузере, и он активно поддерживается в актуальном состоянии с учетом новейших функций веб-платформы. Кроме того, теперь это значительно быстрее! 😉
Используйте образцы, которые можно использовать в качестве репрезентативных рабочих нагрузок, и посмотрите, что вы сможете найти!
2. Избегайте сложной иерархии вызовов
По возможности не делайте график вызовов слишком сложным. При наличии сложных иерархий вызовов легко снизить производительность и сложно понять, почему ваш код работает именно так, а это затрудняет внесение улучшений.
3. Определите ненужную работу
Устаревшие кодовые базы обычно содержат код, который больше не нужен. В нашем случае устаревший и ненужный код занимал значительную часть общего времени загрузки. Его удаление было самым простым решением.
4. Используйте структуры данных правильно
Используйте структуры данных для оптимизации производительности, а также учитывайте затраты и компромиссы, которые приносит каждый тип структуры данных, при принятии решения о том, какую из них использовать. Это не только пространственная сложность самой структуры данных, но и временная сложность применимых операций.
5. Кэшируйте результаты, чтобы избежать дублирования работы при выполнении сложных или повторяющихся операций.
Если выполнение операции требует больших затрат, имеет смысл сохранить ее результаты до следующего раза, когда они понадобятся. Это также имеет смысл делать, если операция выполняется много раз, даже если каждое отдельное время не требует особых затрат.
6. Отложите некритическую работу
Если выходные данные задачи не нужны немедленно и выполнение задачи расширяет критический путь, рассмотрите возможность отложить ее, лениво вызывая ее, когда ее выходные данные действительно необходимы.
7. Используйте эффективные алгоритмы для больших входных данных
Для больших входных данных решающее значение приобретают алгоритмы оптимальной временной сложности. В данном примере мы не рассматривали эту категорию, но их важность вряд ли можно переоценить.
8. Бонус: сравните свои конвейеры
Чтобы убедиться, что ваш развивающийся код остается быстрым, разумно отслеживать его поведение и сравнивать его со стандартами. Таким образом, вы заранее выявляете регрессии и повышаете общую надежность, настраивая себя на долгосрочный успех.
,Независимо от того, какой тип приложения вы разрабатываете, оптимизация его производительности, быстрая загрузка и плавное взаимодействие имеют решающее значение для удобства пользователя и успеха приложения. Один из способов сделать это — проверить активность приложения с помощью инструментов профилирования, чтобы увидеть, что происходит внутри приложения во время его работы в течение определенного временного окна. Панель «Производительность» в DevTools — отличный инструмент профилирования для анализа и оптимизации производительности веб-приложений. Если ваше приложение работает в Chrome, оно дает вам подробный визуальный обзор того, что делает браузер во время выполнения вашего приложения. Понимание этого действия может помочь вам выявить закономерности, узкие места и проблемные точки производительности, над которыми можно действовать, чтобы повысить производительность.
В следующем примере показано, как использовать панель «Производительность» .
Настройка и воссоздание нашего сценария профилирования
Недавно мы поставили перед собой цель сделать панель «Производительность» более производительной. В частности, мы хотели, чтобы он быстрее загружал большие объемы данных о производительности. Это имеет место, например, при профилировании длительных или сложных процессов или при сборе данных с высокой степенью детализации. Для этого сначала необходимо было понять, как работает приложение и почему оно работает именно так, что было достигнуто с помощью инструмента профилирования.
Как вы, возможно, знаете, DevTools сам по себе является веб-приложением. Таким образом, его можно профилировать с помощью панели «Производительность» . Чтобы профилировать эту панель, вы можете открыть DevTools, а затем открыть другой экземпляр DevTools, прикрепленный к ней. В Google эта настройка известна как DevTools-on-DevTools .
Когда настройка готова, необходимо воссоздать и записать сценарий, который будет профилироваться. Во избежание путаницы исходное окно DevTools будет называться « первым экземпляром DevTools», а окно, проверяющее первый экземпляр, будет называться « вторым экземпляром DevTools».

Во втором экземпляре DevTools панель «Производительность », которая в дальнейшем будет называться панелью производительности , наблюдает за первым экземпляром DevTools, чтобы воссоздать сценарий, который загружает профиль.
На втором экземпляре DevTools запускается живая запись, а на первом экземпляре профиль загружается из файла на диске. Загружается большой файл, чтобы точно профилировать производительность обработки больших входных данных. Когда оба экземпляра завершают загрузку, данные профилирования производительности, обычно называемые трассировкой , отображаются во втором экземпляре DevTools панели производительности, загружающей профиль.
Исходное состояние: выявление возможностей для улучшения
После завершения загрузки на следующем снимке экрана на нашем втором экземпляре панели производительности наблюдалось следующее. Сосредоточьтесь на активности основного потока, которая отображается под дорожкой с надписью Main . Можно увидеть, что в карте пламени есть пять больших групп активности. К ним относятся задачи, загрузка которых занимает больше всего времени. Общее время выполнения этих задач составило примерно 10 секунд . На следующем снимке экрана панель производительности используется для фокусировки на каждой из этих групп действий и просмотра того, что можно найти.

Первая группа действий: ненужная работа
Стало очевидно, что первая группа действий представляла собой устаревший код, который все еще работал, но в действительности не был нужен. По сути, все, что находится под зеленым блоком с надписью processThreadEvents
, было напрасной тратой усилий. Это была быстрая победа. Удаление этого вызова функции сэкономило около 1,5 секунд времени. Прохладный!
Вторая группа активности
Во второй группе действий решение было не таким простым, как в первой. Вызов buildProfileCalls
занял около 0,5 секунды, и этой задачи нельзя было избежать.

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

Чтобы проверить это подозрение, мы использовали для расследования панель «Память» (еще одна панель в DevTools, отличная от секции «Память» на панели производительности). На панели «Память» был выбран тип профилирования «Выборка распределения», при котором записывался снимок кучи для панели производительности, загружающей профиль ЦП.

На следующем снимке экрана показан собранный снимок кучи.

Из этого снимка кучи было замечено, что класс Set
потребляет много памяти. Проверив точки вызова, выяснилось, что мы без необходимости присваивали свойства типа Set
объектам, которые создавались в больших объемах. Эти затраты накапливались, и потреблялось много памяти до такой степени, что приложение часто аварийно завершало работу при больших входных данных.
Наборы полезны для хранения уникальных элементов и предоставляют операции, использующие уникальность их содержимого, например дедупликацию наборов данных и обеспечение более эффективного поиска. Однако в этих функциях не было необходимости, поскольку сохраняемые данные гарантированно были уникальными из источника. Таким образом, в наборах изначально не было необходимости. Чтобы улучшить распределение памяти, тип свойства был изменен с Set
на простой массив. После применения этого изменения был сделан еще один снимок кучи, и наблюдалось уменьшение выделения памяти. Несмотря на то, что это изменение не привело к значительному увеличению скорости, дополнительным преимуществом было то, что приложение реже зависало.

Третья группа действий: взвешивание компромиссов в структуре данных
Третий раздел своеобразен: на флейм-диаграмме видно, что он состоит из узких, но высоких столбцов, которые обозначают глубокие вызовы функций и в данном случае глубокие рекурсии. В общей сложности этот раздел длился около 1,4 секунды. Глядя на нижнюю часть этого раздела, стало очевидно, что ширина этих столбцов определялась длительностью одной функции: appendEventAtLevel
, что предполагало, что это может быть узким местом.
В реализации функции appendEventAtLevel
выделяется одна вещь. Для каждой отдельной записи входных данных (которая в коде называется «событием») на карту добавлялся элемент, который отслеживал вертикальное положение записей временной шкалы. Это было проблематично, поскольку количество хранившихся предметов было очень большим. Карты обеспечивают быстрый поиск по ключам, но это преимущество не предоставляется бесплатно. По мере того, как карта становится больше, добавление к ней данных может, например, стать дорогостоящим из-за повторного хеширования. Эта стоимость становится заметной, когда на карту последовательно добавляется большое количество предметов.
/**
* Adds an event to the flame chart data at a defined vertical level.
*/
function appendEventAtLevel (event, level) {
// ...
const index = data.length;
data.push(event);
this.indexForEventMap.set(event, index);
// ...
}
Мы экспериментировали с другим подходом, который не требовал от нас добавления элемента на карту для каждой записи в диаграмме пламени. Улучшение было значительным, подтверждая, что узкое место действительно было связано с накладными расходами, возникающими при добавлении всех данных на карту. Время, которое потребовалось группе действий, сократилось примерно с 1,4 секунды до примерно 200 миллисекунд.
До:

После:

Четвертая группа действий: отсрочка некритической работы и кэширования данных для предотвращения дублирования работы.
Увеличив это окно, можно увидеть, что имеется два практически идентичных блока вызовов функций. Посмотрев на имена вызываемых функций, вы можете сделать вывод, что эти блоки состоят из кода, строящего деревья (например, с такими именами, как refreshTree
или buildChildren
). Фактически, соответствующий код создает древовидные представления в нижнем ящике панели. Что интересно, эти древовидные представления не отображаются сразу после загрузки. Вместо этого пользователю необходимо выбрать древовидное представление (вкладки «Снизу вверх», «Дерево вызовов» и «Журнал событий» в ящике) для отображения деревьев. Более того, как видно из скриншота, процесс построения дерева выполнялся дважды.

На этом изображении мы выявили две проблемы:
- Некритическая задача тормозила производительность времени загрузки. Пользователям не всегда нужны его результаты. Таким образом, задача не является критичной для загрузки профиля.
- Результат этих задач не кэшировался. Поэтому деревья были рассчитаны дважды, хотя данные не менялись.
Мы начали с того, что отложили вычисление дерева до момента, когда пользователь вручную открывал древовидное представление. Только тогда стоит заплатить цену за создание этих деревьев. Общее время выполнения этого дважды составило около 3,4 секунды, поэтому отсрочка существенно увеличила время загрузки. Мы все еще работаем над кэшированием задач такого типа.
Пятая группа действий: по возможности избегайте сложных иерархий вызовов.
Присмотревшись к этой группе, стало ясно, что определенная цепочка вызовов вызывалась неоднократно. Один и тот же паттерн появился 6 раз в разных местах диаграммы пламени, а общая продолжительность этого окна составила около 2,4 секунды!

Связанный код, вызываемый несколько раз, — это часть, которая обрабатывает данные, которые будут отображаться на «мини-карте» (обзор активности временной шкалы в верхней части панели). Непонятно, почему это происходило несколько раз, но уж точно не обязательно, чтобы это происходило 6 раз! Фактически, выходные данные кода должны оставаться текущими, если никакой другой профиль не загружен. Теоретически код должен запускаться только один раз.
В ходе расследования было обнаружено, что связанный код был вызван из-за того, что несколько частей в конвейере загрузки прямо или косвенно вызывали функцию, вычисляющую мини-карту. Это связано с тем, что сложность графа вызовов программы со временем менялась, и по незнанию к этому коду добавлялось больше зависимостей. Быстрого решения этой проблемы не существует. Способ решения зависит от архитектуры рассматриваемой кодовой базы. В нашем случае пришлось немного уменьшить сложность иерархии вызовов и добавить проверку, предотвращающую выполнение кода, если входные данные остались неизменными. После реализации мы получили такой взгляд на график:

Обратите внимание, что выполнение рендеринга мини-карты происходит дважды, а не один раз. Это связано с тем, что для каждого профиля рисуются две мини-карты: одна для обзора в верхней части панели, а другая для раскрывающегося меню, которое выбирает видимый в данный момент профиль из истории (каждый элемент в этом меню содержит обзор выбранного профиля). Тем не менее, эти два имеют одно и то же содержимое, поэтому один из них можно использовать повторно для другого.
Поскольку обе эти мини-карты представляют собой изображения, нарисованные на холсте, пришлось использовать утилиту холста drawImage
и последующий запуск кода только один раз, чтобы сэкономить дополнительное время. В результате этих усилий продолжительность группы сократилась с 2,4 секунды до 140 миллисекунд.
Заключение
После применения всех этих исправлений (и еще пары более мелких кое-где) изменение графика загрузки профиля выглядело следующим образом:
До:

После:

Время загрузки после улучшений составило 2 секунды, а это означает, что улучшение примерно на 80% было достигнуто с относительно небольшими усилиями, поскольку большая часть того, что было сделано, состояла из быстрых исправлений. Конечно, правильное определение того, что делать на начальном этапе, было ключевым моментом, и панель производительности стала для этого подходящим инструментом.
Также важно подчеркнуть, что эти цифры относятся к профилю, используемому в качестве объекта исследования. Профиль был нам интересен тем, что был особенно большим. Тем не менее, поскольку конвейер обработки одинаков для каждого профиля, достигнутое значительное улучшение применимо к каждому профилю, загруженному на панель производительности.
Вынос
Из этих результатов можно извлечь некоторые уроки с точки зрения оптимизации производительности вашего приложения:
1. Используйте инструменты профилирования для выявления закономерностей производительности во время выполнения.
Инструменты профилирования невероятно полезны для понимания того, что происходит в вашем приложении во время работы, особенно для определения возможностей для повышения производительности. Панель Performance в Chrome Devtools является отличным вариантом для веб-приложений, так как это нативный инструмент для веб-профилирования в браузере, и он активно поддерживается, чтобы он был актуален с последними функциями веб-платформы. Кроме того, теперь это значительно быстрее! 😉
Используйте образцы, которые можно использовать в качестве представительных рабочих нагрузок, и посмотрите, что вы можете найти!
2. Избегайте сложных иерархий вызовов
Когда это возможно, избегайте усложнения вашего графа вызовов. С сложными иерархиями вызовов легко представить регрессии производительности и трудно понять, почему ваш код работает так, как он является, затрудняя улучшение.
3. Определите ненужную работу
Стареющие кодовые базы обычно содержат код, который больше не нужен. В нашем случае Legacy и ненужный код принимал значительную часть общего времени загрузки. Удаление этого было самым низким фруктом.
4. Используйте структуры данных соответствующим образом
Используйте структуры данных для оптимизации производительности, но также понимайте затраты и компромиссы, которые каждый тип структуры данных приносит при принятии решения о том, какие из них использовать. Это не только сложность пространства самой структуры данных, но и сложность времени применимых операций.
5. Результаты кэша, чтобы избежать дублирования работы для сложных или повторяющихся операций
Если операция стоит дорого, чтобы выполнить, имеет смысл хранить свои результаты в следующий раз, когда это необходимо. Также имеет смысл сделать это, если операция выполняется много раз, даже если каждое отдельное время не особенно дорого.
6. Отложить некритическую работу
Если вывод задачи не требуется немедленно, и выполнение задачи расширяет критический путь, рассмотрите возможность отложить его, лениво назвав его, когда его вывод фактически необходим.
7. Используйте эффективные алгоритмы на больших входах
Для крупных входов алгоритмы оптимальной сложности времени становятся решающими. В этом примере мы не изучили эту категорию, но их важность вряд ли можно переоценить.
8. Бонус: сравнивать ваши трубопроводы
Чтобы убедиться, что ваш развивающийся код остается быстрым, целесообразно отслеживать поведение и сравнить его со стандартами. Таким образом, вы активно выявляете регрессии и повышаете общую надежность, устанавливая вас для долгосрочного успеха.
,Независимо от того, какой тип приложения вы разрабатываете, оптимизация его производительности и обеспечение быстрого загрузки и предлагает плавное взаимодействие, имеет решающее значение для пользовательского опыта и успеха приложения. Один из способов сделать это - осмотреть активность приложения, используя инструменты профилирования, чтобы увидеть, что происходит под капотом, когда оно работает во временном окне. Панель Performance в Devtools - отличный инструмент профилирования для анализа и оптимизации производительности веб -приложений. Если ваше приложение работает в Chrome, оно дает вам подробный визуальный обзор того, что делает браузер при выполнении вашего приложения. Понимание этой деятельности может помочь вам определить шаблоны, узкие места и точки доступа, которые вы можете действовать, для повышения производительности.
Следующий пример подходит вам, используя панель Performance .
Настройка и воссоздание нашего сценария профилирования
Недавно мы ставим цель, чтобы сделать панель Performance более эффективной. В частности, мы хотели, чтобы он быстрее загружал большие объемы данных о производительности. Это относится, например, при профилировании длительных или сложных процессов или получения данных высокой гранулярности. Чтобы достичь этого, впервые было необходимо понимание того , как выполнялось приложение и почему оно выполнялось, что было достигнуто с помощью инструмента профилирования.
Как вы, возможно, знаете, DevTools сам по себе является веб -приложением. Таким образом, он может быть профилирован с использованием панели производительности . Чтобы профилировать саму панель, вы можете открыть Devtools, а затем открыть еще один экземпляр Devtools, прикрепленный к ней. В Google эта настройка известна как Devtools-On-Devtools .
С готовой настройкой, сценарий, который будет представлен, должен быть воссоздан и записан. Чтобы избежать путаницы, исходное окно Devtools будет упоминаться как « первый экземпляр Devtools», а окно, которое проверяет первый экземпляр, будет упоминаться как « второй экземпляр Devtools».

Во втором экземпляре Devtools панель производительности , которая будет называться панелью PERF с этого момента, вызывает первый экземпляр Devtools, чтобы воссоздать сценарий, который загружает профиль.
Во втором экземпляре Devtools запускается живая запись, в то время как в первую очередь профиль загружается из файла на диске. Большой файл загружается для точного профиля производительности обработки больших входов. Когда оба экземпляра завершают загрузку, данные профилирования производительности - совсем, называемые трассировкой - видны во втором экземпляре Devtools панели PRF, загружающей профиль.
Первоначальное состояние: определение возможностей для улучшения
После того, как загрузка завершилась, на следующем скриншоте наблюдалось следующее на нашем втором экземпляре Perf Panel. Сосредоточьтесь на активности основного потока, которая видно под дорожкой, помеченной Main . Видно, что в таблице пламени есть пять больших групп активности. Они состоят из задач, где загрузка занимает больше всего времени. Общее время этих задач составило приблизительно 10 секунд . На следующем скриншоте панель производительности используется для сосредоточения на каждой из этих групп деятельности, чтобы увидеть, что можно найти.

Первая группа деятельности: ненужная работа
Стало очевидно, что первой группой деятельности была устаревшая кодекс, который все еще работал, но на самом деле не нужна. По сути, все под зеленым блоком с надписью processThreadEvents
было потраченным впустую. Это было быстрой победой. Удаление этой функции вызовов сохранено около 1,5 секунды времени. Прохладный!
Вторая группа деятельности
Во второй группе активности решение было не таким простым, как с первым. buildProfileCalls
заняли около 0,5 секунды, и эта задача не была тем, чего можно было избежать.

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

Чтобы продолжить это подозрение, мы использовали панель памяти (еще одна панель в Devtools, отличную от ящика памяти на панели PERF) для изучения. На панели памяти был выбран тип профилирования «Выборка выборов», который записал снимки кучи для панели PERF, загружающей профиль ЦП.

На следующем скриншоте показан снимки кучи, который был собран.

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

Группа третьей деятельности: взвешивание компромиссов структуры данных
Третий раздел особенный: в таблице пламени вы можете увидеть, что он состоит из узких, но высоких колонн, которые обозначают глубокие вызовы функции и глубокие рекурсии в этом случае. В общей сложности этот раздел длился около 1,4 секунды. Глядя на дно этого раздела, было очевидно, что ширина этих столбцов была определена продолжительностью одной функции: appendEventAtLevel
, которая предполагала, что это может быть узким местом
Внутри реализации функции appendEventAtLevel
одна вещь выделялась. Для каждого отдельного ввода данных на входе (который известен в коде как «событие»), к карте был добавлен элемент, которая отслеживала вертикальное положение записей временной шкалы. Это было проблематично, потому что количество хранимых предметов было очень большим. Карты быстрые для поисков на основе ключей, но это преимущество не поступает бесплатно. По мере того, как карта становится все больше, добавление данных, например, может стать дорогим из -за перефразирования. Эта стоимость становится заметной, когда большие объемы предметов добавляются к карте последовательно.
/**
* Adds an event to the flame chart data at a defined vertical level.
*/
function appendEventAtLevel (event, level) {
// ...
const index = data.length;
data.push(event);
this.indexForEventMap.set(event, index);
// ...
}
Мы экспериментировали с другим подходом, который не требовал, чтобы мы добавили элемент в карту для каждой записи на пламенной диаграмме. Улучшение было значительным, подтверждая, что узкое место действительно было связано с накладными расходом, понесенные путем добавления всех данных на карту. Время, в течение которого группа деятельности взяла сокращение примерно с 1,4 секунды до 200 миллисекунд.
До:

После:

Четвертая группа деятельности: откладывание некритических данных о работе и кеша
Увеличение этого окна, можно увидеть, что существуют два почти идентичных блока функциональных вызовов. Глядя на название именованных функций, вы можете сделать вывод, что эти блоки состоят из кода, который строит деревья (например, с такими именами, как refreshTree
или buildChildren
). Фактически, связанный код - это тот, который создает представления дерева в нижнем ящике панели. Интересно, что эти виды деревьев не показаны сразу после загрузки. Вместо этого пользователь должен выбрать представление дерева (вкладки «снизу вверх», «дерево вызовов» и «Журнал событий» в ящике) для показа деревьев. Кроме того, как вы можете сказать на скриншоте, процесс построения деревьев был выполнен дважды.

Есть две проблемы, которые мы определили с этой картиной:
- Некритическая задача заключалась в том, чтобы препятствовать производительности времени нагрузки. Пользователи не всегда нуждаются в его выводе. Таким образом, задача не является критической для загрузки профиля.
- Результат этих задач не был кэширован. Вот почему деревья были рассчитаны дважды, несмотря на то, что данные не меняются.
Мы начали с отложенного расчета дерева, когда пользователь вручную открыл представление дерева. Только тогда стоит заплатить цену создания этих деревьев. Общее время работы этого дважды составляло около 3,4 секунды, поэтому откладывание оно изменило значительную разницу во время загрузки. Мы все еще рассматриваем кэширование этих типов задач.
Пятая группа деятельности: избегайте сложных иерархий вызовов, когда это возможно
Присмотревшись к этой группе, было ясно, что конкретная вызовая цепочка вызывалась неоднократно. Тот же шаблон появился 6 раз в разных местах в пламенной графике, и общая продолжительность этого окна составляла около 2,4 секунды!

Связанный код, называемый несколько раз, является той частью, которая обрабатывает данные, которые будут отображаться на «Минимали» (обзор деятельности временной шкалы в верхней части панели). Не ясно, почему это происходило несколько раз, но это, конечно, не должно было случиться 6 раз! Фактически, вывод кода должен оставаться текущим, если другой профиль не загружен. Теоретически, код должен работать только один раз.
После расследования было обнаружено, что связанный код был вызван как следствие нескольких частей в трубопроводе загрузки прямо или косвенно вызывает функцию, которая вычисляет минимум. Это связано с тем, что сложность графика вызовов программы развивалась с течением времени, и больше зависимостей от этого кода были добавлены неосознанно. Там нет быстрого решения для этой проблемы. Способ решить это зависит от архитектуры рассматриваемой кодовой базы. В нашем случае нам пришлось немного уменьшить сложность иерархии вызовов и добавить чек, чтобы предотвратить выполнение кода, если входные данные остались неизменными. После реализации этого мы получили этот взгляд на временную шкалу:

Обратите внимание, что выполнение минимального рендеринга происходит дважды, а не один раз. Это связано с тем, что для каждого профиля нарисовано два минимума: один для обзора в верхней части панели, а другой для раскрывающегося меню, которое выбирает в настоящее время видимый профиль из истории (каждый элемент в этом меню содержит обзор выбранного профиля). Тем не менее, эти два имеют одинаковый контент, поэтому один должен быть в состоянии повторно использовать для другого.
Поскольку эти минимумы являются оба изображением, нарисованными на холсте, это было вопросом использования утилиты Canvas Canvas drawImage
, а затем запустить код только один раз, чтобы сохранить дополнительное время. В результате этих усилий продолжительность группы была сокращена с 2,4 секунды до 140 миллисекунд.
Заключение
После применения всех этих исправлений (и нескольких других небольших здесь и там) изменение временной шкалы загрузки профиля выглядело следующим образом:
До:

После:

Время загрузки после улучшений составило 2 секунды, что означает, что улучшение около 80% было достигнуто с относительно низкими усилиями, поскольку большая часть того, что было сделано, состояло из быстрых исправлений. Конечно, правильно определить, что делать изначально, было ключевым, и панель PERF была правильным инструментом для этого.
Также важно подчеркнуть, что эти цифры являются особенными для профиля, используемого в качестве предмета исследования. Профиль был интересным для нас, потому что он был особенно большим. Тем не менее, поскольку обработка конвейер является одинаковым для каждого профиля, достигнутое значительное улучшение относится к каждому профилю, загруженному на панели PRF.
Вынос
Есть несколько уроков, которые можно извлечь из этих результатов с точки зрения оптимизации производительности вашего приложения:
1. Используйте инструменты профилирования для определения шаблонов производительности среды выполнения.
Инструменты профилирования невероятно полезны для понимания того, что происходит в вашем приложении во время работы, особенно для определения возможностей для повышения производительности. Панель Performance в Chrome Devtools является отличным вариантом для веб-приложений, так как это нативный инструмент для веб-профилирования в браузере, и он активно поддерживается, чтобы он был актуален с последними функциями веб-платформы. Кроме того, теперь это значительно быстрее! 😉
Используйте образцы, которые можно использовать в качестве представительных рабочих нагрузок, и посмотрите, что вы можете найти!
2. Избегайте сложных иерархий вызовов
Когда это возможно, избегайте усложнения вашего графа вызовов. С сложными иерархиями вызовов легко представить регрессии производительности и трудно понять, почему ваш код работает так, как он является, затрудняя улучшение.
3. Определите ненужную работу
Стареющие кодовые базы обычно содержат код, который больше не нужен. В нашем случае Legacy и ненужный код принимал значительную часть общего времени загрузки. Удаление этого было самым низким фруктом.
4. Используйте структуры данных соответствующим образом
Используйте структуры данных для оптимизации производительности, но также понимайте затраты и компромиссы, которые каждый тип структуры данных приносит при принятии решения о том, какие из них использовать. Это не только сложность пространства самой структуры данных, но и сложность времени применимых операций.
5. Результаты кэша, чтобы избежать дублирования работы для сложных или повторяющихся операций
Если операция стоит дорого, чтобы выполнить, имеет смысл хранить свои результаты в следующий раз, когда это необходимо. Также имеет смысл сделать это, если операция выполняется много раз, даже если каждое отдельное время не особенно дорого.
6. Отложить некритическую работу
Если вывод задачи не требуется немедленно, и выполнение задачи расширяет критический путь, рассмотрите возможность отложить его, лениво назвав его, когда его вывод фактически необходим.
7. Используйте эффективные алгоритмы на больших входах
Для крупных входов алгоритмы оптимальной сложности времени становятся решающими. В этом примере мы не изучили эту категорию, но их важность вряд ли можно переоценить.
8. Бонус: сравнивать ваши трубопроводы
Чтобы убедиться, что ваш развивающийся код остается быстрым, целесообразно отслеживать поведение и сравнить его со стандартами. Таким образом, вы активно выявляете регрессии и повышаете общую надежность, устанавливая вас для долгосрочного успеха.
,Независимо от того, какой тип приложения вы разрабатываете, оптимизация его производительности и обеспечение быстрого загрузки и предлагает плавное взаимодействие, имеет решающее значение для пользовательского опыта и успеха приложения. Один из способов сделать это - осмотреть активность приложения, используя инструменты профилирования, чтобы увидеть, что происходит под капотом, когда оно работает во временном окне. Панель Performance в Devtools - отличный инструмент профилирования для анализа и оптимизации производительности веб -приложений. Если ваше приложение работает в Chrome, оно дает вам подробный визуальный обзор того, что делает браузер при выполнении вашего приложения. Понимание этой деятельности может помочь вам определить шаблоны, узкие места и точки доступа, которые вы можете действовать, для повышения производительности.
Следующий пример подходит вам, используя панель Performance .
Настройка и воссоздание нашего сценария профилирования
Недавно мы ставим цель, чтобы сделать панель Performance более эффективной. В частности, мы хотели, чтобы он быстрее загружал большие объемы данных о производительности. Это относится, например, при профилировании длительных или сложных процессов или получения данных высокой гранулярности. Чтобы достичь этого, впервые было необходимо понимание того , как выполнялось приложение и почему оно выполнялось, что было достигнуто с помощью инструмента профилирования.
Как вы, возможно, знаете, DevTools сам по себе является веб -приложением. Таким образом, он может быть профилирован с использованием панели производительности . Чтобы профилировать саму панель, вы можете открыть Devtools, а затем открыть еще один экземпляр Devtools, прикрепленный к ней. В Google эта настройка известна как Devtools-On-Devtools .
С готовой настройкой, сценарий, который будет представлен, должен быть воссоздан и записан. Чтобы избежать путаницы, исходное окно Devtools будет упоминаться как « первый экземпляр Devtools», а окно, которое проверяет первый экземпляр, будет упоминаться как « второй экземпляр Devtools».

Во втором экземпляре Devtools панель производительности , которая будет называться панелью PERF с этого момента, вызывает первый экземпляр Devtools, чтобы воссоздать сценарий, который загружает профиль.
Во втором экземпляре Devtools запускается живая запись, в то время как в первую очередь профиль загружается из файла на диске. Большой файл загружается для точного профиля производительности обработки больших входов. Когда оба экземпляра завершают загрузку, данные профилирования производительности - совсем, называемые трассировкой - видны во втором экземпляре Devtools панели PRF, загружающей профиль.
Первоначальное состояние: определение возможностей для улучшения
После того, как загрузка завершилась, на следующем скриншоте наблюдалось следующее на нашем втором экземпляре Perf Panel. Сосредоточьтесь на активности основного потока, которая видно под дорожкой, помеченной Main . Видно, что в таблице пламени есть пять больших групп активности. Они состоят из задач, где загрузка занимает больше всего времени. Общее время этих задач составило приблизительно 10 секунд . На следующем скриншоте панель производительности используется для сосредоточения на каждой из этих групп деятельности, чтобы увидеть, что можно найти.

Первая группа деятельности: ненужная работа
Стало очевидно, что первой группой деятельности была устаревшая кодекс, который все еще работал, но на самом деле не нужна. По сути, все под зеленым блоком с надписью processThreadEvents
было потраченным впустую. Это было быстрой победой. Удаление этой функции вызовов сохранено около 1,5 секунды времени. Прохладный!
Вторая группа деятельности
Во второй группе активности решение было не таким простым, как с первым. buildProfileCalls
заняли около 0,5 секунды, и эта задача не была тем, чего можно было избежать.

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

Чтобы продолжить это подозрение, мы использовали панель памяти (еще одна панель в Devtools, отличную от ящика памяти на панели PERF) для изучения. На панели памяти был выбран тип профилирования «Выборка выборов», который записал снимки кучи для панели PERF, загружающей профиль ЦП.

На следующем скриншоте показан снимки кучи, который был собран.

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

Группа третьей деятельности: взвешивание компромиссов структуры данных
Третий раздел особенный: в таблице пламени вы можете увидеть, что он состоит из узких, но высоких колонн, которые обозначают глубокие вызовы функции и глубокие рекурсии в этом случае. В общей сложности этот раздел длился около 1,4 секунды. Глядя на дно этого раздела, было очевидно, что ширина этих столбцов была определена продолжительностью одной функции: appendEventAtLevel
, которая предполагала, что это может быть узким местом
Внутри реализации функции appendEventAtLevel
одна вещь выделялась. Для каждого отдельного ввода данных на входе (который известен в коде как «событие»), к карте был добавлен элемент, которая отслеживала вертикальное положение записей временной шкалы. Это было проблематично, потому что количество хранимых предметов было очень большим. Карты быстрые для поисков на основе ключей, но это преимущество не поступает бесплатно. По мере того, как карта становится все больше, добавление данных, например, может стать дорогим из -за перефразирования. Эта стоимость становится заметной, когда большие объемы предметов добавляются к карте последовательно.
/**
* Adds an event to the flame chart data at a defined vertical level.
*/
function appendEventAtLevel (event, level) {
// ...
const index = data.length;
data.push(event);
this.indexForEventMap.set(event, index);
// ...
}
Мы экспериментировали с другим подходом, который не требовал, чтобы мы добавили элемент в карту для каждой записи на пламенной диаграмме. Улучшение было значительным, подтверждая, что узкое место действительно было связано с накладными расходом, понесенные путем добавления всех данных на карту. Время, в течение которого группа деятельности взяла сокращение примерно с 1,4 секунды до 200 миллисекунд.
До:

После:

Четвертая группа деятельности: откладывание некритических данных о работе и кеша
Увеличение этого окна, можно увидеть, что существуют два почти идентичных блока функциональных вызовов. Глядя на название именованных функций, вы можете сделать вывод, что эти блоки состоят из кода, который строит деревья (например, с такими именами, как refreshTree
или buildChildren
). Фактически, связанный код - это тот, который создает представления дерева в нижнем ящике панели. Интересно, что эти виды деревьев не показаны сразу после загрузки. Вместо этого пользователь должен выбрать представление дерева (вкладки «снизу вверх», «дерево вызовов» и «Журнал событий» в ящике) для показа деревьев. Кроме того, как вы можете сказать на скриншоте, процесс построения деревьев был выполнен дважды.

Есть две проблемы, которые мы определили с этой картиной:
- Некритическая задача заключалась в том, чтобы препятствовать производительности времени нагрузки. Пользователи не всегда нуждаются в его выводе. Таким образом, задача не является критической для загрузки профиля.
- Результат этих задач не был кэширован. Вот почему деревья были рассчитаны дважды, несмотря на то, что данные не меняются.
Мы начали с отложенного расчета дерева, когда пользователь вручную открыл представление дерева. Только тогда стоит заплатить цену создания этих деревьев. Общее время работы этого дважды составляло около 3,4 секунды, поэтому откладывание оно изменило значительную разницу во время загрузки. Мы все еще рассматриваем кэширование этих типов задач.
Пятая группа деятельности: избегайте сложных иерархий вызовов, когда это возможно
Присмотревшись к этой группе, было ясно, что конкретная вызовая цепочка вызывалась неоднократно. Тот же шаблон появился 6 раз в разных местах в пламенной графике, и общая продолжительность этого окна составляла около 2,4 секунды!

Связанный код, называемый несколько раз, является той частью, которая обрабатывает данные, которые будут отображаться на «Минимали» (обзор деятельности временной шкалы в верхней части панели). Не ясно, почему это происходило несколько раз, но это, конечно, не должно было случиться 6 раз! Фактически, вывод кода должен оставаться текущим, если другой профиль не загружен. Теоретически, код должен работать только один раз.
После расследования было обнаружено, что связанный код был вызван как следствие нескольких частей в трубопроводе загрузки прямо или косвенно вызывает функцию, которая вычисляет минимум. Это связано с тем, что сложность графика вызовов программы развивалась с течением времени, и больше зависимостей от этого кода были добавлены неосознанно. Там нет быстрого решения для этой проблемы. Способ решить это зависит от архитектуры рассматриваемой кодовой базы. В нашем случае нам пришлось немного уменьшить сложность иерархии вызовов и добавить чек, чтобы предотвратить выполнение кода, если входные данные остались неизменными. После реализации этого мы получили этот взгляд на временную шкалу:

Note that the minimap rendering execution occurs twice, not once. This is because there are two minimaps being drawn for every profile: one for the overview on top of the panel, and another for the drop-down menu that selects the currently visible profile from the history (every item in this menu contains an overview of the profile it selects). Nonetheless, these two have the exact same content, so one should be able to reused for the other.
Since these minimaps are both images drawn on a canvas, it was a matter of using the drawImage
canvas utility , and subsequently running the code only once to save some extra time. As a result of this effort, the group's duration was reduced from 2.4 seconds to 140 milliseconds.
Заключение
After having applied all these fixes (and a couple of other smaller ones here and there), the change of the profile loading timeline looked as follows:
До:

После:

The load time after the improvements was 2 seconds, meaning that an improvement of about 80% was achieved with relatively low effort, since most of what was done consisted of quick fixes. Of course, properly identifying what to do initially was key, and the perf panel was the right tool for this.
It's also important to highlight that these numbers are particular to a profile being used as a subject of study. The profile was interesting to us because it was particularly large. Nonetheless, since the processing pipeline is the same for every profile, the significant improvement achieved applies to every profile loaded in the perf panel.
Вынос
There are some lessons to take away from these results in terms of performance optimization of your application:
1. Make use of profiling tools to identify runtime performance patterns
Profiling tools are incredibly useful to understand what's happening in your application while it's running, especially to identify opportunities to improve performance. The Performance panel in Chrome DevTools is a great option for web applications since it's the native web profiling tool in the browser, and it's actively maintained to be up-to-date with the latest web platform features. Also, it's now significantly faster! 😉
Use samples that can be used as representative workloads and see what you can find!
2. Avoid complex call hierarchies
When possible, avoid making your call graph too complicated. With complex call hierarchies, it's easy to introduce performance regressions and hard to understand why your code is running the way it is, making it hard to land improvements.
3. Identify unnecessary work
It's common for aging codebases to contain code that's no longer needed. In our case, legacy and unnecessary code was taking a significant portion of the total loading time. Removing it was the lowest-hanging fruit.
4. Use data structures appropriately
Use data structures to optimize performance, but also understand the costs and trade-offs each type of data structure brings when deciding which ones to use. This isn't only the space complexity of the data structure itself, but also time complexity of the applicable operations.
5. Cache results to avoid duplicate work for complex or repetitive operations
If the operation is costly to execute, it makes sense to store its results for the next time it's needed. It also makes sense to do this if the operation is done many times—even if each individual time isn't particularly costly.
6. Defer non-critical work
If the output of a task isn't needed immediately and the task execution is extending the critical path, consider deferring it by lazily calling it when its output is actually needed.
7. Use efficient algorithms on large inputs
For large inputs, optimal time complexity algorithms become crucial. We didn't look into this category in this example, but their importance can hardly be overstated.
8. Bonus: benchmark your pipelines
To make sure your evolving code remains fast, it's wise to monitor the behavior and compare it against standards. This way, you proactively identify regressions and improve the overall reliability, setting you up for long-term success.