Como melhorar o desempenho do ngRepeat em um dataset enorme (angular.js)?

Eu tenho um enorme dataset de vários milhares de linhas com cerca de 10 campos cada, cerca de 2MB de dados. Eu preciso exibi-lo no navegador. A abordagem mais direta (buscar dados, colocá-los em $scope , deixar ng-repeat="" fazer seu trabalho”) funciona bem, mas congela o navegador por cerca de meio minuto quando começa a inserir nós no DOM. Como devo abordar esse problema?

Uma opção é append linhas ao $scope incrementalmente e esperar que o ngRepeat termine de inserir um ngRepeat no DOM antes de passar para o próximo. Mas o AFAIK ngRepeat não reporta quando termina a “repetição”, por isso vai ser feio.

Outra opção é dividir os dados no servidor em páginas e buscá-los em várias solicitações, mas isso é ainda mais feio.

Eu olhei através da documentação Angular em busca de algo como ng-repeat="data in dataset" ng-repeat-steps="500" , mas não encontrei nada. Eu sou relativamente novo em maneiras angulares, então é possível que eu esteja perdendo completamente o ponto. Quais são as melhores práticas para isso?

Concordo com @ AndreM96 que a melhor abordagem é exibir apenas uma quantidade limitada de linhas, mais rápido e melhor UX, isso poderia ser feito com uma paginação ou com uma rolagem infinita.

Rolagem infinita com Angular é realmente simples com filtro limitTo . Você só tem que definir o limite inicial e quando o usuário pede mais dados (estou usando um botão para simplificar) você aumenta o limite.

 
{{d}}
//the controller $scope.totalDisplayed = 20; $scope.loadMore = function () { $scope.totalDisplayed += 20; }; $scope.data = data;

Aqui está um JsBin .

Essa abordagem pode ser um problema para os telefones, porque geralmente eles ficam paralisados ​​ao rolar muitos dados, então, neste caso, acho que uma paginação se encheckbox melhor.

Para fazer isso, você precisará do limitePara filtrar e também de um filtro personalizado para definir o ponto inicial dos dados que estão sendo exibidos.

Aqui está um JSBin com uma paginação.

A abordagem mais moderna – e possivelmente a mais escalável – para superar esses desafios com grandes conjuntos de dados é incorporada pela abordagem da diretiva collectionRepeat da Ionic e de outras implementações semelhantes. Um termo chique para isso é ‘occlusion culling’ , mas você pode resumir como: não apenas limitar a contagem de elementos DOM renderizados a um número paginado arbitrário (mas ainda alto) como 50, 100, 500 … , limite apenas a quantos elementos o usuário puder ver .

Se você faz algo parecido com o que é comumente conhecido como “rolagem infinita”, você está reduzindo um pouco a contagem inicial de DOM, mas ela incha rapidamente depois de algumas atualizações, porque todos esses novos elementos são colocados na parte inferior. Rolagem vem para um rastreamento, porque a rolagem é toda sobre a contagem de elementos. Não há nada infinito nisso.

Considerando que, a abordagem collectionRepeat é usar apenas tantos elementos quanto caberá na viewport e, em seguida, reciclá-los . À medida que um elemento gira fora da vista, ele é separado da tree de renderização, recarregado com dados para um novo item na lista e, em seguida, reconectado à tree de renderização na outra extremidade da lista. Este é o caminho mais rápido conhecido pelo homem para obter novas informações dentro e fora do DOM, fazendo uso de um conjunto limitado de elementos existentes, ao invés do ciclo tradicional de criar / destruir … criar / destruir. Usando essa abordagem, você pode realmente implementar um deslocamento infinito .

Note que você não precisa usar o Ionic para usar / hack / adaptar collectionRepeat ou qualquer outra ferramenta como esta. É por isso que eles chamam de código aberto. 🙂 (Dito isto, a equipe Ionic está fazendo algumas coisas bastante engenhosas, dignas de sua atenção.)


Há pelo menos um excelente exemplo de fazer algo muito semelhante no React. Apenas em vez de reciclar os elementos com conteúdo atualizado, você simplesmente está optando por não renderizar nada na tree que não está à vista. Está brilhando rapidamente em 5000 itens, embora sua implementação muito simples de POC permita um pouco de flicker …


Também … para ecoar alguns dos outros posts, usar o track by é seriamente útil, mesmo com conjuntos de dados menores. Considere isso obrigatório.

Eu recomendo ver isso:

Otimizando AngularJS: 1200ms a 35ms

eles fizeram uma nova diretiva otimizando ng-repeat em 4 partes:

Otimização nº 1: elementos DOM do cache

Otimização nº 2: observadores agregados

Otimização nº 3: adiar a criação de elementos

Otimização nº 4: ignore os observadores em busca de elementos ocultos

o projeto está aqui no github:

Uso:

1- inclua esses arquivos em seu aplicativo de página única:

  • core.js
  • scalyr.js
  • slyEvaluate.js
  • slyRepeat.js

2- adicionar dependência de módulo:

 var app = angular.module("app", ['sly']); 

3- substitua ng-repeat

  ..... 

Apreciar!

Além de todas as dicas acima, como faixa e loops menores, esse também me ajudou muito

  

essa parte do código imprimiria o nome assim que fosse carregado e pararia de assisti-lo depois disso. Similarmente, para ng-repeats, pode ser usado como

 
{{::stock.name}}

no entanto, ele só funciona para o AngularJS versão 1.3 e superior. De http://www.befundoo.com/blog/optimizing-ng-repeat-in-angularjs/

Se todas as suas linhas tiverem a mesma altura, você deve definitivamente dar uma olhada na virtualização do ng-repeat: http://kamilkp.github.io/angular-vs-repeat/

Esta demo parece muito promissora (e suporta rolagem inercial)

Você pode usar “acompanhar por” para aumentar o desempenho:

 

Mais rápido que:

 

ref: https://www.airpair.com/angularjs/posts/angularjs-performance-large-applications

A rolagem virtual é outra maneira de melhorar o desempenho da rolagem ao lidar com listas grandes e grandes conjuntos de dados.

Uma forma de implementar isso é usando o Material Angular md-virtual-repeat conforme demonstrado nesta demonstração com 50.000 itens

Retirado diretamente da documentação da repetição virtual:

A repetição virtual é um substituto limitado para ng-repeat que renderiza apenas nós dom suficientes para preencher o contêiner e reciclá-los à medida que o usuário rola.

Regra No.1: Nunca deixe o usuário esperar por nada.

Isso em mente, uma página de vida que precisa de 10 segundos parece muito mais rápido do que esperar 3 segundos antes de uma canvas em branco e obter tudo de uma vez.

Então, em vez de tornar a página mais rápida, deixe a página parecer rápida, mesmo que o resultado final seja mais lento:

 function applyItemlist(items){ var item = items.shift(); if(item){ $timeout(function(){ $scope.items.push(item); applyItemlist(items); }, 0); // <-- try a little gap of 10ms } } 

O código acima mostra que a lista está crescendo linha a linha e é sempre mais lenta do que renderizar de uma vez. Mas para o usuário , parece ser mais rápido.

Outra versão @Steffomio

Em vez de adicionar cada item individualmente, podemos adicionar itens por blocos.

 // chunks function from here: // http://stackoverflow.com/questions/8495687/split-array-into-chunks#11764168 var chunks = chunk(folders, 100); //immediate display of our first set of items $scope.items = chunks[0]; var delay = 100; angular.forEach(chunks, function(value, index) { delay += 100; // skip the first chuck if( index > 0 ) { $timeout(function() { Array.prototype.push.apply($scope.items,value); }, delay); } }); 

Às vezes, o que aconteceu, você obtém os dados do servidor (ou back-end) em poucos ms (por exemplo, estou assumindo 100ms), mas leva mais tempo para ser exibido em nossa página da Web (digamos que exibição).

Então, o que está acontecendo aqui é 800ms Está levando apenas para renderizar página web.

O que eu fiz na minha aplicação web é, eu usei paginação (ou você pode usar a rolagem infinita também) para exibir a lista de dados. Digamos que eu esteja mostrando 50 dados / página.

Então eu não vou carregar todos os dados de uma só vez, apenas 50 dados que estou carregando inicialmente, o que leva apenas 50ms (estou assumindo aqui).

assim, o tempo total aqui diminuiu de 900ms para 150ms, uma vez que o usuário solicita a próxima página e exibe os próximos 50 dados, e assim por diante.

Espero que isso ajude você a melhorar o desempenho. Muito bem sucedida

 Created a directive (ng-repeat with lazy loading) 

que carrega os dados quando atinge a parte inferior da página e remove metade dos dados carregados anteriormente e quando atinge a parte superior da div novamente, os dados anteriores (dependendo do número da página) serão carregados removendo metade dos dados atuais. No momento, apenas dados limitados estão presentes, o que pode levar a um melhor desempenho, em vez de renderizar dados inteiros sob carga.

CÓDIGO HTML:

     AngularJS Plunker        
{{row["sno"]}} {{row["id"]}} {{row["name"]}}

CÓDIGO angular:

 var app = angular.module('plunker', []); var x; ListController.$inject = ['$scope', '$timeout', '$q', '$templateCache']; function ListController($scope, $timeout, $q, $templateCache) { $scope.itemsPerPage = 40; $scope.lastPage = 0; $scope.maxPage = 100; $scope.data = []; $scope.pageNumber = 0; $scope.makeid = function() { var text = ""; var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (var i = 0; i < 5; i++) text += possible.charAt(Math.floor(Math.random() * possible.length)); return text; } $scope.DataFormFunction = function() { var arrayObj = []; for (var i = 0; i < $scope.itemsPerPage*$scope.maxPage; i++) { arrayObj.push({ sno: i + 1, id: Math.random() * 100, name: $scope.makeid() }); } $scope.totalData = arrayObj; $scope.totalData = $scope.totalData.filter(function(a,i){ a.index = i; return true; }) $scope.rowData = $scope.totalData.slice(0, $scope.itemsperpage); } $scope.DataFormFunction(); $scope.onRowSelected = function(row,index){ console.log(row,index); } } angular.module('plunker').controller('ListController', ListController).directive('datafilter', function($compile) { return { restrict: 'EAC', scope: { data: '=', totalData: '=totaldata', pageNumber: '=pagenumber', searchdata: '=', defaultinput: '=', selectedrow: '&', filterflag: '=', totalFilterData: '=' }, link: function(scope, elem, attr) { //scope.pageNumber = 0; var tempData = angular.copy(scope.totalData); scope.totalPageLength = Math.ceil(scope.totalData.length / +attr.itemsperpage); console.log(scope.totalData); scope.data = scope.totalData.slice(0, attr.itemsperpage); elem.on('scroll', function(event) { event.preventDefault(); // var scrollHeight = angular.element('#customTable').scrollTop(); var scrollHeight = document.getElementById("customTable").scrollTop /*if(scope.filterflag && scope.pageNumber != 0){ scope.data = scope.totalFilterData; scope.pageNumber = 0; angular.element('#customTable').scrollTop(0); }*/ if (scrollHeight < 100) { if (!scope.filterflag) { scope.scrollUp(); } } if (angular.element(this).scrollTop() + angular.element(this).innerHeight() >= angular.element(this)[0].scrollHeight) { console.log("scroll bottom reached"); if (!scope.filterflag) { scope.scrollDown(); } } scope.$apply(scope.data); }); /* * Scroll down data append function */ scope.scrollDown = function() { if (scope.defaultinput == undefined || scope.defaultinput == "") { //filter data append condition on scroll scope.totalDataCompare = scope.totalData; } else { scope.totalDataCompare = scope.totalFilterData; } scope.totalPageLength = Math.ceil(scope.totalDataCompare.length / +attr.itemsperpage); if (scope.pageNumber < scope.totalPageLength - 1) { scope.pageNumber++; scope.lastaddedData = scope.totalDataCompare.slice(scope.pageNumber * attr.itemsperpage, (+attr.itemsperpage) + (+scope.pageNumber * attr.itemsperpage)); scope.data = scope.totalDataCompare.slice(scope.pageNumber * attr.itemsperpage - 0.5 * (+attr.itemsperpage), scope.pageNumber * attr.itemsperpage); scope.data = scope.data.concat(scope.lastaddedData); scope.$apply(scope.data); if (scope.pageNumber < scope.totalPageLength) { var divHeight = $('.assign-list').outerHeight(); if (!scope.moveToPositionFlag) { angular.element('#customTable').scrollTop(divHeight * 0.5 * (+attr.itemsperpage)); } else { scope.moveToPositionFlag = false; } } } } /* * Scroll up data append function */ scope.scrollUp = function() { if (scope.defaultinput == undefined || scope.defaultinput == "") { //filter data append condition on scroll scope.totalDataCompare = scope.totalData; } else { scope.totalDataCompare = scope.totalFilterData; } scope.totalPageLength = Math.ceil(scope.totalDataCompare.length / +attr.itemsperpage); if (scope.pageNumber > 0) { this.positionData = scope.data[0]; scope.data = scope.totalDataCompare.slice(scope.pageNumber * attr.itemsperpage - 0.5 * (+attr.itemsperpage), scope.pageNumber * attr.itemsperpage); var position = +attr.itemsperpage * scope.pageNumber - 1.5 * (+attr.itemsperpage); if (position < 0) { position = 0; } scope.TopAddData = scope.totalDataCompare.slice(position, (+attr.itemsperpage) + position); scope.pageNumber--; var divHeight = $('.assign-list').outerHeight(); if (position != 0) { scope.data = scope.TopAddData.concat(scope.data); scope.$apply(scope.data); angular.element('#customTable').scrollTop(divHeight * 1 * (+attr.itemsperpage)); } else { scope.data = scope.TopAddData; scope.$apply(scope.data); angular.element('#customTable').scrollTop(divHeight * 0.5 * (+attr.itemsperpage)); } } } } }; }); 

Demonstração com diretiva

 Another Solution: If you using UI-grid in the project then same implementation is there in UI grid with infinite-scroll. 

Dependendo da altura da divisão, ele carrega os dados e, após a rolagem, novos dados serão anexados e os dados anteriores serão removidos.

Código HTML:

     AngularJS Plunker         

Código Angular:

 var app = angular.module('plunker', ['ui.grid', 'ui.grid.infiniteScroll', 'ui.grid.selection']); var x; angular.module('plunker').controller('ListController', ListController); ListController.$inject = ['$scope', '$timeout', '$q', '$templateCache']; function ListController($scope, $timeout, $q, $templateCache) { $scope.itemsPerPage = 200; $scope.lastPage = 0; $scope.maxPage = 5; $scope.data = []; var request = { "startAt": "1", "noOfRecords": $scope.itemsPerPage }; $templateCache.put('ui-grid/selectionRowHeaderButtons', "
 
" ); $templateCache.put('ui-grid/selectionSelectAllButtons', "
" ); $scope.gridOptions = { infiniteScrollDown: true, enableSorting: false, enableRowSelection: true, enableSelectAll: true, //enableFullRowSelection: true, columnDefs: [{ field: 'sno', name: 'sno' }, { field: 'id', name: 'ID' }, { field: 'name', name: 'My Name' }], data: 'data', onRegisterApi: function(gridApi) { gridApi.infiniteScroll.on.needLoadMoreData($scope, $scope.loadMoreData); $scope.gridApi = gridApi; } }; $scope.gridOptions.multiSelect = true; $scope.makeid = function() { var text = ""; var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (var i = 0; i < 5; i++) text += possible.charAt(Math.floor(Math.random() * possible.length)); return text; } $scope.abc = function() { var a = $scope.search; x = $scope.searchData; $scope.data = x.filter(function(arr, y) { return arr.name.indexOf(a) > -1 }) console.log($scope.data); if ($scope.gridApi.grid.selection.selectAll) $timeout(function() { $scope.gridApi.selection.selectAllRows(); }, 100); } $scope.loadMoreData = function() { var promise = $q.defer(); if ($scope.lastPage < $scope.maxPage) { $timeout(function() { var arrayObj = []; for (var i = 0; i < $scope.itemsPerPage; i++) { arrayObj.push({ sno: i + 1, id: Math.random() * 100, name: $scope.makeid() }); } if (!$scope.search) { $scope.lastPage++; $scope.data = $scope.data.concat(arrayObj); $scope.gridApi.infiniteScroll.dataLoaded(); console.log($scope.data); $scope.searchData = $scope.data; // $scope.data = $scope.searchData; promise.resolve(); if ($scope.gridApi.grid.selection.selectAll) $timeout(function() { $scope.gridApi.selection.selectAllRows(); }, 100); } }, Math.random() * 1000); } else { $scope.gridApi.infiniteScroll.dataLoaded(); promise.resolve(); } return promise.promise; }; $scope.loadMoreData(); $scope.getProductList = function() { if ($scope.gridApi.selection.getSelectedRows().length > 0) { $scope.gridOptions.data = $scope.resultSimulatedData; $scope.mySelectedRows = $scope.gridApi.selection.getSelectedRows(); //<--Property undefined error here console.log($scope.mySelectedRows); //alert('Selected Row: ' + $scope.mySelectedRows[0].id + ', ' + $scope.mySelectedRows[0].name + '.'); } else { alert('Select a row first'); } } $scope.getSelectedRows = function() { $scope.mySelectedRows = $scope.gridApi.selection.getSelectedRows(); } $scope.headerButtonClick = function() { $scope.selectAll = $scope.grid.selection.selectAll; } }

Demonstração com grade de interface do usuário com demonstração de rolagem infinita

para grandes conjuntos de dados e múltiplos valores, é melhor usar ng-options vez de ng-repeat .

ng-repeat é lento porque faz um loop em todos os valores próximos, mas ng-options simplesmente são exibidas na opção de seleção.

 ng-options='state.StateCode as state.StateName for state in States'> 

muito mais rápido que