Недавно во время разработки NG я решил проблему с производительностью с помощью инструмента профилирования Chrome и хотел бы поделиться и записать этот процесс здесь.
Это внутренний продукт моей компании, и я все еще использую Angular 1.x (не намерен объяснять, почему все еще 1.x, в общем, этого достаточно). Обычно он запускается путем получения данных из серверной части и последующего отображения некоторых списков или диаграмм для использования в работе. И моя работа - просто показать данные в двух разных представлениях. Поскольку оба представления являются списками (вы можете просто подумать, что это один и тот же массив, упорядоченный по разным свойствам, но более того), поэтому я использую один и тот же шаблон. Код, подобный следующему:
<!-- HTML template example, uses bootstrap accordion component -->
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-default" ng-repeat="item in fooLists">
<div class="panel-heading" role="tab">
<h3>
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#fooPanel" aria-expanded="true">
</a>
</h3>
</div>
<div id="fooPanel" class="panel-collapse collapse" role="tabpanel" aria-expanded="true">
<div class="panel-body">...</div>
</div>
</div>
</div>
# Angular controller example for view switching buttons, uses coffeescript syntax
$scope.switchView = (view) ->
switch view
when 'fooView1'
...
$scope.fooLists = getView1List()
when 'fooView2'
...
$scope.fooLists = getView2List()
return
Теперь я думаю, что все ясно. Та же $scope переменная и тот же шаблон, легко и просто. Теперь, когда я нажимал кнопку переключателя, чтобы переключиться на другое представление, происходили странные вещи: это занимало 2 или 3 секунды или около того! Но в моем списке всего несколько сотен пунктов, а не тысячи, он не должен быть таким медленным. Мне очень интересно узнать причину, я запустил инструмент профилирования Chrome, и давайте попробуем выяснить, почему.
1. Во-первых, сначала диагностируйте причину.
Посмотрите на всю картину профилирования до и после того, как я нажал кнопку переключения просмотра.

На верхней временной шкале мы видим длинную желтую область, которая указывает на то, что было задействовано много сценариев (javascript). Это была только причина? Еще раз взгляните на нижнюю вкладку «Сводка».

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

setTimeout активность занимала большую часть времени. Затем я расширил дерево стека вызовов до самого глубокого вызова.

OMG, неудивительно, что это было… наконец, мой собственный код, так называемый xxxTooptip.js, который представляет собой директиву, основанную на популярном компоненте всплывающей подсказки Bootstrap. Я сомневался, что это может быть мое неправильное использование, а не ошибка этого общедоступного компонента. Сначала проверьте код.

И мое использование в шаблоне:
<!-- Html the two view-switching buttons -->
<div class="btn-group" role="group" aria-label="..." xxx-tooltip>
<button class="btn btn-sm" ng-click="switchView('view1')" data-toggle="tooltip" data-original-title="xxx" >view1</button>
<button class="btn btn-sm" ng-click="switchView('view2')" data-toggle="tooltip" data-original-title="xxx">view2</button>
</div>
Из кода мы видим, что он настраивает и привязывает обработчик кликов к каждому элементу, имеющему атрибут data-toggle="tooltip". Таким образом, каждый раз, когда связывается элемент с директивой xxxTooltip, кнопка переключения будет привязана к новому, но повторяющемуся обработчику. Что, если мы применим эту директиву к каждому элементу длинного списка? К сожалению, это только мой случай. Обработчики нажатия кнопки переключения вызывались неэффективно сотни раз! Что ж, это действительно плохой случай, хотя это типичный фрагмент кода, скопированный с веб-ресурсов. Я считал, что это по крайней мере одна из причин проблемы с производительностью, и в целом нам следует ограничить объем директивы, чтобы она не повлияла на другие. Так что попробуй исправить.
2. Первый раунд рефакторинга кода.
Я переписал директиву следующим образом:
# use coffeescript
link: (scope, element, attrs) ->
tooltipElem = element.find('[data-toggle="tooltip"]')
tooltipElem.tooltip(
container: 'body'
placement: 'top'
html: true
trigger: 'hover'
)
tooltipElem.on('click', ->
$(this).tooltip('hide')
return
)
На этот раз с обработчиком кликов будут связаны только его собственные дочерние элементы этой директивы. Так что, когда мы нажмем кнопку, наверняка будет вызван только один обработчик. Перекомпилируйте и посмотрите изменения.

Наиболее трудоемкие сценарии исчезли, как и ожидалось, и стали быстрее примерно на 60%. Хорошее улучшение. Но я все еще чувствовал паузу при переключении между кнопками. Сейчас наибольшая активность - Major GC. Я знаю, что сборщик мусора предназначен для сборки мусора, но почему здесь был запущен сборщик мусора? Давайте копать дальше.

Я снова расширил стек вызовов и обнаружил, что это ngRepeat. Помните, мы использовали одну и ту же переменную $scope и тот же шаблон? Хотя сборщик мусора может быть сложным в деталях, этот шаблон мультиплексирования должен быть основной причиной, поскольку мы всегда переназначили $scope.fooLists, старый массив был скопирован, а затем переупорядочен DOM… разумный анализ. Так как это исправить? Как насчет использования отдельных переменных и шаблонов? Таким образом, мы должны предоставить еще одну директиву NG ngShow для управления видимостью двух представлений, и она с меньшей вероятностью будет запускать сборщик мусора часто, поскольку все переменные хранятся в памяти. На самом деле это только мое окончательное решение. Продолжать.
3. Второй раунд рефакторинга кода.
Переписал шаблоны и контроллер.
<!-- templates -->
<div class="panel-group accordion" role="tablist" aria-multiselectable="true" ng-show="view === 'view1'">
<div class="panel panel-default" ng-repeat="item in fooLists1">
<div class="panel-heading" role="tab">
<h3>
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#fooPanel" aria-expanded="true">
</a>
</h3>
</div>
<div id="fooPanel" class="panel-collapse collapse" role="tabpanel" aria-expanded="true">
<div class="panel-body">...</div>
</div>
</div>
</div>
<div class="panel-group accordion" role="tablist" aria-multiselectable="true" ng-show="view === 'view2'">
<div class="panel panel-default" ng-repeat="item in fooLists2">
<div class="panel-heading" role="tab">
<h3>
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#fooPanel" aria-expanded="true">
</a>
</h3>
</div>
<div id="fooPanel" class="panel-collapse collapse" role="tabpanel" aria-expanded="true">
<div class="panel-body">...</div>
</div>
</div>
</div>
# Angular controller
$scope.view = 'view1'
$scope.switchView = (view) ->
$scope.view = view
return
Таким образом, мы должны подготовить два массива $scope.fooLists1 и $scope.fooLists2. Каждый раз, когда мы получаем данные из бэкэнда, переключение будет просто изменением стилей CSS. Конечный результат:

GC уменьшился примерно на 80%, и теперь я чувствовал себя плавно, когда я переключал представления. Хотя у него есть минусы, например доплата за заполнение очередного списка в самом начале, думаю, я добился желаемой оптимизации. Теперь все работает хорошо.
Заключение
Из этого успешного профилирования я думаю, что при разработке NG следует отметить как минимум три вещи:
- Постарайтесь максимально ограничить объем ваших директив. Директива в дизайне должна сохранять высокую согласованность и свободную взаимосвязь, как и общее правило для любых других шаблонов проектирования программного обеспечения.
- Готовьте разные шаблоны и используйте
ngShowчасто эффективный способ. По сути, вы используете больше памяти для повышения скорости. Иногда это может быть компромисс, но попробовать стоит. - Доверьтесь инструменту профилирования. Используйте это всякий раз, когда вы сталкиваетесь с проблемами производительности.
Спасибо за то, что прочитали этот довольно подробный пост, и надеюсь поделиться с вами другими в будущем 😃