Acelerando o loop $ digest do AngularJS

A ligação mágica de HTML / JS do Angular depende de um algoritmo de verificação suja muito eficiente. No entanto, quando você finalmente atinge seu limite, seu aplicativo está fadado a ser lento. Quando tudo mais falhar, você pode simplesmente trapacear.

Primeiro, uma cartilha sobre verificação de sujeira AngularJS

Quando você adiciona um ng-clickmanipulador, o Angular chama sua função e espera pacientemente até você retornar. Em seguida, ele deve adivinhar todas as alterações feitas em seus escopos. Escopos são objetos JS normais: nada de especial acontece quando você os modifica e o Angular não tem uma maneira fácil de rastrear suas alterações.

Sempre que você usa uma {{model.value}}ligação, o AngularJS a transforma em uma função (usando o módulo $ parse) e a adiciona a uma lista privada: scope.$$watchers( scope.$watchfaz o mesmo). Para descobrir mudanças, o Angular não tem escolha a não ser chamar todos os observadores $$ para ver se seus resultados mudaram em comparação com as chamadas anteriores. Geralmente, há pelo menos 1 observador por elemento HTML e um aplicativo normal provavelmente usará milhares deles.

De qualquer forma, saiba que sempre que algo acontecer em seu aplicativo, o Angular chamará todos os seus observadores $$: centenas, senão milhares de funções JS (a maioria delas geradas instantaneamente). À medida que seu aplicativo cresce, isso vai demorar cada vez mais e pode resultar em travamentos perceptíveis e embaraçosos .

Surpreendentemente, isso geralmente não é um problema, mesmo que o documento o aconselhe a não mostrar mais de 2.000 elementos de uma vez. Observe que o AngularJS 2.0 provavelmente trará um desempenho muito maior para isso.

Tenho a infelicidade de escrever a maior parte do meu código em um MacBook Air de 11 pés que está começando a mostrar sua idade e já executei esse limite com frequência.

O problema da longa lista

Suponha que você tenha uma longa lista composta por ditos, vários milhares de células. Para ficar abaixo do limite de 2.000 elementos, você pode ficar tentado a adicionar um manipulador ao scrollevento, determinar quais células são visíveis e ocultar as outras. Infelizmente, isso fará seu aplicativo rastrear mal (~ 5 fps). O motivo é que o evento de rolagem é disparado com muita frequência: possivelmente em todos os quadros disponíveis. Se o loop de resumo for concluído em, digamos, 100 ms, ele será aceitável para responder a um clique, mas bem acima dos 16 ms necessários para 60 fps.

Existem algumas otimizações óbvias, como eliminar o evento, mas terá um impacto limitado. Descobri que quanto mais você debounce (quanto mais você espera antes de 2 digeridos sucessivos), mais células visíveis você precisa (para que o usuário não tenha tempo para rolar além das células visíveis). No geral, você deve encontrar um equilíbrio entre fps baixos (debounce curto), congelamentos (debounce longo) ou um aplicativo com falhas.

Você também pode tentar limitar o número de observadores $$ com o angular-once , mas ele desabilitará efetivamente o AngularJS e você também pode ficar com o jQuery.

Meu truque: desativar seletivamente $$ observadores

Supondo que tenhamos esta marcação:

<ul ng-controller="listCtrl">
<li ng-repeat="item in visibleList">{{lots of bindings}}</li>
</ul>

E este código:

app.controller('listCtrl', function ($scope, $element) {
$element
.on('scroll', function (e) {
$scope
.visibleList = getVisibleElements(e);
$scope
.$digest();
});
});

Durante o $ digest, você está interessado apenas em alterações visibleList, não em itens individuais. Ainda assim, o Angular ainda interrogará cada observador para mudanças.

Então, escrevi esta diretiva muito simples:

app.directive('faSuspendable', function () {
return {
link
: function (scope) {
// Heads up: this might break is suspend/resume called out of order
// or if watchers are added while suspended
var watchers;

scope
.$on('suspend', function () {
watchers
= scope.$$watchers;
scope
.$$watchers = [];
});

scope
.$on('resume', function () {
if (watchers)
scope
.$$watchers = watchers;

// discard our copy of the watchers
watchers
= void 0;
});
}
};
});

E mudei meu código para:

<ul ng-controller="listCtrl">
<li fa-suspendable ng-repeat="item in visibleList">{{lots of bindings}}</li>
</ul>

app.controller('listCtrl', function ($scope, $element) {

$element.on('scroll', function (e) {

$scope.visibleList = getVisibleElements(e);


$scope.$broadcast('suspend');

$scope.$digest();

$scope.$broadcast('resume');

});

});

O que ele faz é simplesmente mascarar temporariamente os observadores de itens individuais. Em vez de passar por centenas de observadores, o Angular apenas verifica se os elementos foram adicionados ou removidos da minha visibleList. O aplicativo voltou instantaneamente para 60fps durante a rolagem!

O bom é que todos os outros eventos que ainda estão funcionando são normais. Você pode ter seu bolo E comê-lo:

  • Monitore os eventos de rolagem de perto para ocultar todos os elementos invisíveis e reduzir significativamente o número de observadores.
  • Tenha ciclos de $ digest gerenciáveis ​​para todos os outros eventos.