Ir para o conteúdo
Engenharia de Grades de Dados Rápidas: Lições da Otimização de Ignite UI para Registros de Dados de 1M+

Engenharia de Grades de Dados Rápidas: Lições da Otimização de Ignite UI para Registros de Dados de 1M+

O desempenho no grid não é só sobre velocidade aqui. É sobre consistência sob carga pesada de dados. Quando uma rede trava durante operações de dados, ela parece lenta e pouco confiável. Em fluxos de trabalho de tomada de decisão em tempo real, essa falta de confiabilidade se torna um problema.

28min read

Para desenvolvedores que desenvolvem sistemas de finanças, bancos, ERP e outros que consomem muitos dados, a grade de dados é frequentemente a principal fronteira de desempenho – o "hot loop" onde a ordenação e filtragem entre grandes conjuntos de dados competem pelo tempo da thread principal. Nesses casos, pequenas ineficiências rapidamente se tornam visíveis para o usuário e quebram a interação.

Mas encontramos uma solução. Este post vai demonstrar como otimizamos a ordenação e a filtragem para manter Ignite UI rápido em 1M+ linhas entre frameworks (Angular, React, Blazor, Web Components). Vamos focar na grade de dados concreta, ordenação e filtragem das mudanças que funcionaram e das que não funcionaram.

Vamos ver o que fizemos.

A Realidade Antes da Otimização: Onde as Coisas Começaram a Quebrar

Todo problema de desempenho começa da mesma forma – uma arquitetura que era razoável em uma escala se torna um gargalo em outra. Recursos como ordenação, agrupamento e filtragem da interface do Ignite não foram exceção.

Ordenação: O Custo Oculto da Resolução de Valores

O pipeline de ordenação central funcionava recursivamente, processando cada expressão de ordenação em sequência. Para a ordenação de múltiplas colunas, após ordenar pela expressão primária, agrupava registros de valor igual e ordenava recursivamente cada grupo pela próxima expressão. Limpo, correto e completamente razoável para conjuntos de dados pequenos.

O problema era o resolvedor de valor.

Como a grade suporta múltiplos tipos de dados de coluna – partes de data dos objetos Data, partes de tempo dos objetos Data, strings, números, objetos-chave-valor hierárquicos – toda comparação de valores exigia a resolução do valor do campo em tempo de execução. O resolvedor de valores lidava com percorrido de caminhos, análise de datas, normalização de tempo, análise numérica, tudo em cada comparação. Era chamado duas vezes por operação de comparação – uma vez para cada lado:

compare(recordA, recordB): 

    valA = resolveValue(recordA, field)  // path traversal + date parsing + type coercion 

    valB = resolveValue(recordB, field)  // same cost, every single comparison 

    return compareValues(valA, valB) 

Para uma classificação de comparação padrão, isso éO(nãolognão)O(n log n)comparações, com o resolver chamado duas vezes por comparação. Com 100 mil linhas: 3,4 milhões de chamadas de resolver por coluna ordenada. Em 1 milhão de linhas: 40 milhões de chamadas resolvedor. Cada um fazendo resolução de caminho em tempo de execução e possível análise de datas, sem cache entre as chamadas.

Mas o comparador de classificação não foi o único lugar onde o resolvedor de valor foi invocado. Para ordenação em múltiplas colunas, após ordenar por expressão i, o algoritmo precisava encontrar grupos de valores iguais antes de ordenar por expressão i+1. Essa detecção em grupo iterava sobre cada registro, chamando o resolver uma vez por registro – um adicionalO(não)O(n)Passe por cima do sorte.

Assim, para uma ordenação em duas colunas sobre 1M de linhas, o resolver de valores foi invocado na ordem deO(nãolognão)O(n log n) + O(não)O(n)Às vezes só para a primeira expressão – antes mesmo de a segunda expressão ser tocada.

  • Em 10 mil linhas: imperceptível.
  • Em 100 mil linhas: um atraso perceptível, mas tolerável.
  • Em 1M de linhas: o fio principal travou por vários segundos. Em casos raros, pilhas de chamadas recursivas profundas causavam um excesso de pilha.

Agrupamento: Mesma raiz, custo composto

O agrupamento estende o mesmo padrão recursivo e exige que os dados sejam ordenados primeiro. Dessa forma, o custo do resolver era pago uma vez durante a triagem, depois novamente durante a detecção de limites de grupo.

groupDataRecursive(data, state, level): 

    while i < data.length: 

        group = groupByExpression(data, i, expressions[level]) 

            // resolver called once for group anchor value 

            // resolver called again for every subsequent record in the group 

  

        if level < expressions.length - 1: 

            groupDataRecursive(group, state, level + 1)  // recurse into subgroups 

        else: 

            result = result.concat(...)    // array allocation per group boundary 

Dois custos compostos aqui:

  1. O resolvedor de valores era invocado repetidamente para valores que já haviam sido resolvidos durante a ordenação, sem cache compartilhado entre as duas fases.
  1. Cada limite de grupo produzia novos arranjos via concat e slice, ou seja,  alocações que adicionavam pressão de GC mensurável em escala entre potencialmente milhares de grupos

Filtragem no estilo Excel: Pagando o Custo Total Duas vezes

Filtragem rápida e avançada foram rápidas. O filtro no estilo Excel (ESF) não era, e o motivo era arquitetônico.

Quando o diálogo do ESF abriu, ele acionou um pipeline completo de inicialização de forma síncrona na thread principal:

A animação de abertura do diálogo foi efetivamente pausada até que todas as quatro operações fossem concluídas. Com grandes conjuntos de dados, isso era um congelamento visível para o usuário, o diálogo não parecia estranho. Simplesmente não apareceu até o pipeline terminar.

O problema mais crítico: todo esse pipeline rodava novamente quando o usuário clicava em Aplicar, mesmo que os dados subjacentes não tivessem mudado entre abrir e aplicar:

onApplyClick(): 
    filter data 

    re-run full ESF initialization  // same 4 steps, same cost, same blocking 

    close dialog 

É por isso que o ESF era significativamente mais lento que o filtragem avançado na prática: ele fazia o mesmoO(não)O(n)Trabalhe duas vezes por operação, bloqueando a thread principal em ambas as ocasiões.

Por que "apenas virtualizar mais" não era a resposta

A virtualização garante apenas o número de linhas visíveis a serem renderizadas como nós DOM, independentemente do tamanho do conjunto de dados. É isso que torna viável rolar por 1 milhão de linhas. Mas as operações de dados que determinam o que essas linhas contêm – ordenação, filtragem, agrupamento – correm contra o conjunto de dados completo toda vez. Virtualização não pode ajudar nisso. Todo gargalo acima permanecia no pipeline de dados, antes de uma única linha ser renderizada:

  • O resolver foi chamadoO(nãolognão)O(n log n) + O(não)O(n)Vezes por expressão de ordenação, independentemente de quantas linhas fossem visíveis.
  • Agrupar pagou novamente o custo da resolução além da separação, além da pressão de alocação de concat/fatia entre os limites dos grupos.
  • Todo o pipeline de inicialização do ESF iterava o conjunto de dados completo de forma síncrona, na abertura e novamente no apply.

Virtualização é a ferramenta certa para tornar grandes grades roláveis. Isso não ajuda em nada a tornar a triagem, filtragem e agrupamento rápida. Esses exigiam um tipo diferente de conserto.

Medindo o Problema: Como Avaliamos o Desempenho da Rede

Anedotas como "parece lento" e "parece rápido" são um ponto de partida, não um diagnóstico. Para otimizar com confiança, precisávamos de números reproduzíveis em vez de impressões.

É tentador confiar nos gráficos de chama ou contadores de FPS do DevTools para diagnosticar o desempenho da grade. Mas esses medem todo o pipeline de renderização – detecção de mudanças, atualizações do DOM, layout, o que pode ocultar que o tempo realmente é gasto no pipeline de dados.

Para identificar especificamente o custo do algoritmo, instrumentamos diretamente a lógica de ordenação, agrupamento e filtragem usando um wrapper leve ao redor da API nativa de performance:

startMeasure(‘sorting’) 

        -> run sorting algorithm 

getMeasures(‘sorting’) // returns the duration 

Isso nos deu um tempo de algoritmos de menos de milissegundos isoladamente, sem ruído de renderização ou sobrecarga de detecção de mudanças. Apenas o custo bruto do pipeline de dados. Vale notar: todos os números abaixo foram registrados em modo Angular desenvolvedor. As versões de produção seriam mais rápidas, mas a sobrecarga do modo desenvolvedor é consistente entre as runs, então as diferenças relativas se mantêm.

Os Conjuntos de Dados

Rows: 
        10K / 100K / 1,000,000 
Columns:   
        string - names, categories (with duplicates) 
        number - IDs, prices, quantities (with duplicates) 
        date - formatted date strings (require parsing) 
        time - HH:mm:ss formatted strings (require parsing) 

A presença de valores duplicados nas colunas de ordenação e grupo foi intencional – reflete distribuições realistas dos dados e impacta diretamente o custo de agrupamento, já que mais valores duplicados significam mais detecções de limites de grupo e chamadas recursivas mais profundas. Colunas de data e hora usavam representações de string formatadas. Isso é importante para interpretar os resultados: toda comparação envolvendo essas colunas requer analisar a string em um valor comparável em tempo de execução.

Scenarios and Results 

Em remadas de 10K e 100K, a maioria das operações era aceitável. Com 1 milhão de linhas, a imagem mudou drasticamente:

Scenario Tempo (1M fileiras)
Single column sort – string 3.38s 
Single column sort – number 1.50s 
Multi-column sort – string → number 3.88s 
Agrupamento – coluna de uma única string (ordenar + grupo)3.31s 
Algoritmo de agrupamento apenas (após a ordenação)0.50s 
Agrupamento – duas colunas sobre carga da grade3.86s 
Agrupamento – duas colunas (depois da sorte)1.01s 
ESF open – number column (15K unique values) 1.60s 
ESF open – date column (274 unique values) 5.20s 
ESF open – time column (86K unique values) 6.60s 
ESF apply – number column 1.37s 

Leitura dos Números

Vários padrões surgem imediatamente, e cada um aponta diretamente para um problema arquitetônico específico.

A ordenação domina o custo de agrupamento. O algoritmo de agrupamento sozinho levou 0,50s. Ordenação completa + grupo levaram 3,31s – uma diferença de 6,6x. A lógica de agrupamento em si nunca foi o gargalo. A classificação era, e especificamente o resolvedor de valor sendo chamadoO(nãolognão)O(n log n) Tempos dentro do comparador de classificação.

A ordenação de strings é mais do que duas vezes mais lenta que a ordenação de números (3,38s vs 1,50s). Números se comparam com uma simples subtração. As cadeias passam pelo resolvedor de valores, pela normalização potencial para ordenações insensíveis a maiúsculas e minúsculas e por uma comparação de cadeia. Essa diferença se acumula em ~20 milhões de comparações em 1M de linhas.

A anomalia da data do ESF é o ponto de dados mais revelador. A coluna de data tinha apenas 274 valores únicos – uma lista minúscula comparada a 15K na coluna numérica. Ainda assim, abrir o diálogo ESF levava 5,20s contra 1,60s para a coluna numérica. O culpado não foi a contagem de iterações. Era o cálculo de datas do custo por item. O conjunto de dados completo foi iterado durante a inicialização do ESF, e cada valor passou por análise sintática de strings até a data. Menos valores únicos não ajudavam porque a análise acontecia em todos os registros, não apenas nos únicos. A coluna de tempo (6,60s com 86K valores únicos + análise de strings de tempo) confirma o mesmo padrão: colunas de string formatadas são caras independentemente da cardinalidade.

ESF aberto + ESF aplicado = o custo total pago duas vezes. Para uma coluna numérica, o caso mais barato – isso é 1,60s + 1,37s = ~3s de bloqueio por operação de filtro. Para colunas de data ou hora, o custo combinado seria significativamente pior.

Os números confirmaram o que a revisão da arquitetura sugeriu: o resolvedor de valores, as passagens de agrupamento recursivo e a dupla inicialização do ESF eram os gargalos. Agora tínhamos os dados para provar isso.

Otimização #1: Repensando o Pipeline de Classificação

Com uma linha de base clara estabelecida, o foco mudou para o próprio pipeline de dados. Três mudanças impulsionaram a maior parte da melhoria: aplicar a transformação Schwartziana à ordenação, refatorar a ordenação multi-coluna de recursiva para iterativa, e reestruturar o algoritmo de agrupamento para eliminar tanto a recursão quanto a alocação redundante de arrays.

Fix #1: The Schwartzian Transform 

O comparador de ordenação original resolvia os valores dos campos dentro da própria função de comparação – ou seja, para cada par de registros comparados, o resolvedor de valores rodava duas vezes.

A transformada Schwartziana é uma otimização clássica para chaves de ordenação caras: resolve cada valor uma vez inicialmente, ordena nos valores em cache e depois repede para os registros originais. Isso melhora a resolução de campo a partir deO(nãolognão)O(n log n)ParaO(não)O(n)

// Before: resolve inside comparator - O(n log n) resolver calls 

sort(data, field): 

    data.sort((a, b) => compare(resolveValue(a), resolveValue(b))) 

  

// After: Schwartzian transform - O(n) resolver calls 

sort(data, field): 

    prepared = data.map(record => [record, resolveValue(record, field)])  // O(n) - resolve once 

    prepared.sort(([, valA], [, valB]) => compareValues(valA, valB))      // O(n log n) — compare only 

    return prepared.map(([record]) => record)                              // O(n) - unwrap 

O comparador se torna uma comparação pura de valores, sem resolução de campo, sem percurso de caminho, sem análise de datas. Para o ignoreCase, a chamada de normalização de string entra na fase de mapeamento – resolvida uma vez por registro, não uma vez por lado da comparação.

Para colunas de data e hora, o impacto é especialmente significativo: a análise sintática string-to-date passa de dentro do loop hot comparer para uma única passagem inicial. Em 1 milhão de linhas, essa é a diferença entre ~40 milhões de chamadas de análise e exatamente 1 milhão, que éO(não)O(n)com multiplicador constante de 1, independentemente do tipo de coluna.

Fix #2: Iterative Multi-Column Sorting 

A ordenação original de múltiplas colunas era recursiva: ordenar por expressão 0, encontrar grupos de mesmo valor, ordenar recursivamente cada grupo pela expressão 1, e assim por diante. Correto, mas com dois problemas: profundidade da pilha de chamadas recursivas e o resolvedor de valores sendo chamado novamente dentro da detecção de grupo para cada registro em cada passagem.

A nova abordagem itera para trás por meio de expressões, o que é uma escolha deliberada para manter a estabilidade de ordenação, correspondendo ao comportamento da implementação recursiva original:

// Before: recursive 

sortDataRecursive(data, expressions, index): 

    sort by expressions[index] 

    for each equal-value group: 

        sortDataRecursive(group, expressions, index + 1)  // recursive 

  

// After: iterative - reverse pass maintains stability 

sortData(data, expressions): 

    for i = expressions.length - 1 down to 0: 

        data = expressions[i].strategy.sort(data)    // iterative, no recursion 

Iterar ao contrário significa que a chave de ordenação mais significativa é aplicada por último. Isso se torna o desempate final, e a ordem geral permanece estável. Nenhuma pilha de chamadas recursivas, nenhuma passagem de detecção de grupos intermediária entre expressões; nenhuma chamada adicional de resolvedor. A transformada de Schwartz aplica-se independentemente a cada passagem de expressão.

Correção #3: Agrupamento Iterativo com uma Pilha

O algoritmo de agrupamento tinha duas fontes de custo independentes: a estrutura de chamadas recursivas e as alocações de array concat / fatia em cada limite de grupo. Ambos foram dirigidos juntos.

// Before: recursive with concat/slice 

groupDataRecursive(data, state, level): 

    group = data.slice(start, end)                // allocation per group 

    result = result.concat(groupRow, group)        // allocation per group 

    groupDataRecursive(group, state, level + 1)   // recursive 

  

// After: iterative with explicit stack + direct push 

groupData(data, state): 

    stack = [{ data, level: 0 }] 

    while stack.length > 0: 

        { data, level } = stack.pop() 

        for each group boundary in data: 

            result.push(groupRow)                 // no intermediate allocation 

            result.push(...groupRecords)         // no intermediate allocation 

            if level < expressions.length - 1: 

                stack.push({ data: groupRecords, level: level + 1 }) 

A pré-alocação de array não foi viável aqui porque o número de grupos não é conhecido de antemão. Mas trocar de concat / slice para direct push eliminou as alocações intermediárias de array em cada limite de grupo. Em larga escala, potencialmente ao longo de milhares de limites de grupos, isso fez uma diferença mensurável tanto no tempo de execução quanto na pressão do GC.

Os resultados

Milissegundos brutos contam uma parte da história. A métrica mais importante é a percepção da responsividade:

  • Uma ordenação de string de coluna única em 1M de linhas passou de 3,38s –um congelamento visível e brusco – para 0,42s, imperceptível para a maioria dos usuários
  • A ordenação multi-coluna caiu de 3,88s para 0,57s –usuários que aplicam ordenação sequencial não apresentam mais atrasos de composição
  • O agrupamento de duas colunas na carga da grade passou de 3,86s para 0,88s – a grade parece pronta quase imediatamente

Os ganhos se acumulam no uso real: um usuário que ordena, depois agrupa e depois reordena não está mais esperando vários segundos por cada uma dessas operações. O pipeline roda rápido o suficiente para que a interação pareça contínua, em vez de pontuada por travamentos.

Otimização #2: Filtragem em Escala no Estilo Excel

A triagem e o agrupamento eram os gargalos mais visíveis, mas o filtro estilo Excel tinha seus próprios problemas. Filtragem rápida e filtragem avançada operam diretamente nos dados: um predicado executa cada registro e retorna uma correspondência. Simples, linear, previsível.

Filtragem no estilo Excel é diferente. Antes que o diálogo possa mostrar qualquer coisa, ele precisa construir uma imagem completa dos dados com cada valor único na coluna, formatado para exibição, ordenado e cruzado com o estado atual do filtro. Isso não é apenas uma operação de filtragem. Esse é um pipeline completo de dados, e ele rodava de forma síncrona no thread principal toda vez que o diálogo abre.

Como mencionado acima, a inicialização original de filtragem no estilo Excel fazia quatro passagens sequenciais sobre os dados:

  1. Filtre o conjunto de dados se já forem aplicados filtros antes –O(não)O(n) pass 
  1. Ordene os valores filtrados –O(nãolognão)O(n logn)  
  1. Extrair rótulos + valores de formato –O(não)O(n) pass 
  1. Deduplicate -> build unique items list – O(não)O(n) pass 

A reinicialização do Apply foi a parte mais desperdiçadora: os dados subjacentes não mudaram entre abrir e aplicar, mas todo o pipeline rodou do zero de qualquer forma.

Além do custo duplo, o próprio pipeline tinha uma ineficiência: as etapas 2, 3 e 4 estavam todas operando sobre o conjunto de dados filtrado completo. A ordenação acontecia antes da deduplicação, o que significava que a grade estava potencialmente ordenando milhões de registros quando só precisava ordenar os valores únicos. A extração e a deduplicação de rótulos também eram passagens separadas sobre os mesmos dados, visitando cada valor duas vezes desnecessariamente.

A Anomalia de Data e Hora

A ineficiência era mais visível nas colunas de data e hora. Dos benchmarks em Measuring the Problem:

ColunaUnique values Horário aberto do ESF
Número15k 1.60s 
Data274 5.20s 
Tempo86k 6.60s 

A coluna de data tinha 274 valores únicos – bem menos do que os 15K da coluna numérica – mas demorava 3× mais para abrir. O motivo: a extração de rótulos e a formatação de valores envolviam análise de datas em todo o conjunto de dados, não apenas nos valores únicos. Cada registro era visitado, e cada visita acionava a conversão de string para date. Menos valores únicos não ajudavam porque a análise acontecia durante a passagem completa dos dados, não após a deduplicação.

Correção #1: Eliminar a Inicialização Dupla

A mudança mais impactante foi estrutural: o ESF não reinicializa mais no Apply. A lista de valores únicos construída no abrir é reutilizada diretamente quando o usuário clica em Aplicar. A segunda execução completa do pipeline desapareceu completamente.

// Before 

onApplyClick(): 

    re-run full ESF initialization    // O(n) - redundant 

    close dialog 

  

// After 

onApplyClick(): 

    apply filter using existing list  // O(1) - list already built 

    close dialog 

Correção #2: Deduplicação em Passagem Única com Classificação Diferida

A segunda mudança reestruturou completamente o pipeline, colapsando a extração e a deduplicação de rótulos em uma única passada, e depois ordenando apenas o resultado desduplicado:

// Before: separate passes 

filteredData → sort → extract labels (pass 1) → deduplicate (pass 2) 

  

// After: deduplicate in single pass → sort unique list only 

filteredData (n records) 

    → single pass: 

        resolve + normalize + deduplicate inline   // O(n), parse only for new unique values 

    → unique list (m items) 

    → sort unique list                             // O(m log m) where m <= n 

Duas melhorias acumuladas aqui:

  1. A formatação de rótulos e análise de datas agora só rodam para valores únicos, não para todos os registros do conjunto de dados. Para uma coluna de data com 274 valores únicos em um conjunto de dados de linhas de 1M, essa é a diferença entre 1M de chamadas de análise e 274.
  1. A ordenação agora opera sobre a lista deduplicada, não sobre o conjunto de dados completo filtrado. Com 274 valores únicos, a ordenação é efetivamente instantânea. Mesmo para a coluna de tempo com 86K valores únicos, ordenar 86K itens é muito mais barato do que ordenar 1M – e como cada comparação nesse ordenamento envolve uma análise de string de tempo, reduzir a entrada de ordenação aumenta ainda mais a economia.

Fix #3: Non-Blocking Dialog Open 

A terceira mudança abordou diretamente o desempenho percebido: o diálogo agora se abre imediatamente, antes que o pipeline de dados seja executado. Um indicador de carregamento é exibido enquanto a inicialização é concluída. Isso significa que a interface nunca fica travada esperando por um diálogo que ainda não apareceu. Mesmo que a inicialização leve tempo, o usuário recebe um feedback imediato – o diálogo está aberto e algo está acontecendo.

Fix #4: Debounced Quick Filtering 

Uma melhora menor, mas significativa, no lado do filtro rápido: anteriormente, o filtro era acionado a cada tecla, o que significava que um usuário digitando "Finanças" acionava 7 operações de filtro em rápida sucessão, cada uma iterando o conjunto completo de dados.

// Before: filter on every keystroke 

input: "F"       → filter            // O(n) 

input: "Fi"      → filter            // O(n) 

input: "Fin"     → filter            // O(n) 

... 

  

// After: debounced 

input: "F", "Fi", "Fin", "Fina", "Finan", "Financ", "Finance" 

→ pause detected → filter once       // O(n) - only when user stops typing 

Para grandes conjuntos de dados, isso por si só reduz o número de operações de filtro de thread principal para uma busca típica de 5–10 para 1–2.

Os Resultados

O número de aplicação ESF é particularmente significativo: com 90ms, agora está na mesma faixa de desempenho do filtro rápido e do avançado. Os três modos de filtragem agora são comparáveis em custos pela primeira vez.

O que isso significa na prática

  • O diálogo do ESF aparece imediatamente ao clique. Chega de esperar por um diálogo que não aparece.
  • O tempo total para os dados serem carregados dentro do diálogo do ESF é mais rápido em todos os tipos de coluna. Os usuários passam menos tempo olhando para um indicador de carregamento mesmo quando o conjunto de dados é grande.
  • Aplicar um filtro não repete mais o custo total de inicialização. É praticamente gratuito comparado a antes.
  • A filtragem rápida não martela mais o tópico principal sobre digitação rápida. O debouncing garante que o pipeline rode apenas quando o usuário terminar ou pausar.

Por que essas mudanças funcionam em diferentes frameworks

As melhorias de desempenho abordadas acima foram feitas no código Angular. Mas eles não ficam lá.

One Core, Multiple Frameworks 

A grade do Ignite UI é integrada Angular– utilizável diretamente como um componente nativo de Angular, com acesso total à sintaxe de templates, sistema DI e detecção de alterações do Angular. Também é embalado como um Componente Web usando Angular Elements, tornando-o disponível fora Angular completamente. React e Blazor consomem esse Web Component por meio de wrappers finos específicos do framework que conectam a API do elemento personalizado para React props e parâmetros Blazor, respectivamente.

O pipeline de dados – ordenação, agrupamento, filtragem – está inteiramente na base Angular. Angular Elements o empacota no Web Component como está. React e Blazor nunca toque. Toda melhoria algorítmica feita no código Angular se propaga automaticamente por toda a cadeia. Vale a pena ser preciso sobre o que "wrapper" significa aqui. É uma camada fina de integração, não uma reimplementação.

Por que as melhorias do algoritmo são independentes do framework

A transformada Schwartziana, a pilha iterativa de agrupamento e a deduplicação ESF de passagem única são operações puramente de dados. Eles recebem um array e retornam um array transformado para fora. Eles não têm conhecimento da detecção de mudanças do Angular, do reconciliador do React ou da árvore de renderização do Blazor – e é exatamente por isso que eles se propagam tão limpamente pelas quatro plataformas.

As melhorias são ganhos no motor JavaScript:

  • Fewer resolver calls per sort operation. 
  • Menos alocações intermediárias de arrays por limite de grupo.
  • Less GC pressure across the full pipeline. 
  • Tempo de bloqueio de thread principal mais curto em cada operação de dados.

Nenhum desses conceitos é estrutural. Uma ordenação mais rápida melhora o desempenho independentemente de o resultado ser renderizado por Angular, React, Web Components ou Blazor porque a otimização ocorre na camada de dados antes do framework da interface renderizá-la.

Para desenvolvedores que avaliam qual grade usar: a história de desempenho é a mesma em todos os frameworks porque o motor é o mesmo entre eles. Os números neste post não são Angular números. São números de pipeline de dados, e o pipeline de dados é compartilhado.
 

O que isso significa para equipes empresariais

As vitórias em desempenho de engenharia são fáceis de medir em milissegundos. O impacto deles no negócio é mais difícil de quantificar, mas muito mais significativo, especialmente em escala empresarial, onde as grades de dados não são elementos decorativos de interface, mas a principal interface pela qual analistas, traders e equipes de operações realizam seu trabalho.

Problemas de desempenho em grid de dados geram uma categoria específica e frustrante de tickets de suporte: difíceis de reproduzir, difíceis de diagnosticar e difíceis de fechar. "A grade trava quando eu separo" não é um bug com rastreamento de pilha. É um sintoma de um pipeline que bloqueia o thread principal por vários segundos sob volumes de dados do mundo real.

Ignite UI suporta vinculação remota de dados com ordenação e filtragem que podem ser delegadas a um servidor em vez de executadas do lado do cliente. Para equipes que adotaram operações remotas principalmente porque o desempenho do lado do cliente era inadequado, essas otimizações mudam o cálculo. A ordenação do lado do cliente em 1 milhão de linhas agora é concluída em menos de meio segundo. Para muitos conjuntos de dados corporativos que antes incentivavam as equipes a delegar do lado do servidor, o pipeline do lado do cliente agora é rápido o suficiente para reconsiderar essa decisão.

Em ambientes empresariais – especialmente em serviços financeiros – a percepção de responsividade influencia diretamente a adoção da plataforma. Mover um tipo de 3,38s para 0,42s não é apenas uma melhora de 8× isoladamente. É a diferença entre uma interação que interrompe um fluxo de trabalho e uma que não é registrada como atraso. Essa distinção é importante quando o usuário final decide se a ferramenta vale a pena ser usada.
 

Lições Aprendidas: O Que Fariamos Novamente (e de Forma Diferente)

Os números do antes e depois deste post estão limpos. O processo que os produziu não foi. Veja como esse processo realmente foi.

Nada foi garantido desde o início

Ao iniciar esse trabalho, não havia certeza de que qualquer uma dessas otimizações produziria resultados significativos. A transformada Schwartziana é uma técnica bem conhecida. No entanto, "bem conhecido" não significa "garantido para ajudar nesse contexto." A pilha de agrupamento iterativo parecia promissora no papel, mas refatorações recursivas para iterativas têm histórico de introduzir casos sutis que só aparecem sob formas específicas de dados.

A abordagem era deliberadamente incremental: enfrentando um problema de cada vez, uma medida, e depois decidir se continuaria. O pipeline de seleção veio primeiro. Quando os números voltaram – de 3,38 a 0,42 segundos em uma ordenação de string – validou a direção e justificou continuar com o agrupamento e filtragem. Se a primeira otimização tivesse mostrado ganhos marginais, a estratégia teria mudado.

Isso importa porque o trabalho de performance geralmente é planejado como se os resultados fossem conhecidos antecipadamente. Eles não são. A postura correta é hipótese, medição, decisão, repetição.

A Troca da Memória

A transformação Schwartziana não é gratuita. Ele aloca um array intermediário de pares [record, valor] logo no início – uma entrada por registro. Em 1M de linhas, isso já é uma sobrecarga de memória não trivial antes mesmo de começar a triagem.

Essa foi uma troca consciente: aceitar um uso de pico maior de memória em troca da eliminaçãoO(nãolognão)O(n log n)Resolver chama. Para os casos de uso que essa biblioteca mira, ou seja, grades empresariais rodando em navegadores modernos em hardware capaz, os ganhos de velocidade são significativos e o custo de memória é aceitável.

Mas vale a pena citar explicitamente: se ambientes com restrição de memória algum dia se tornarem um alvo primário, a transformação Schwartziana precisaria ser revisitada. Velocidade e memória puxam em direções opostas aqui, e a implementação atual escolheu velocidade.

Os benchmarks devem refletir o uso real

O conjunto de benchmarks para este trabalho usou conjuntos de dados sintéticos em 1M de linhas (registros gerados com tipos de colunas e distribuições de valores controlados). Esse é o ponto certo de partida para isolar o desempenho algorítmico, mas ele tem um teto.

As duas questões que realmente motivaram esse trabalho vieram de um cliente real: o tempo de abertura do diálogo ESF e o tempo de aplicação do ESF foram relatados como problemas de bloqueio em produção. Quando esses ingressos chegaram, os benchmarks sintéticos confirmaram o problema. O problema já existia antes da multa. Foi preciso um padrão de uso real para revelá-lo.

A lição é simples: benchmarks sintéticos são bons para medir cenários que você já sabe testar. Os dados do cliente encontram aqueles que você não pensou em incluir. Ambos são necessários, e o conjunto de benchmarks deve evoluir para incorporar padrões de uso reais à medida que surgem, não apenas os piores casos sintéticos.

O trabalho de performance nunca é concluído

As melhorias neste post são reais e significativas. Eles também são um retrato fotográfico. O pipeline de dados está mais rápido hoje do que era há seis meses. Daqui a seis meses, existem áreas conhecidas, como análise de datas, virtualização, etc., que se parecerão com o pipeline de ordenação que foi visto antes desse trabalho. Eles serão funcionais, mas com espaço para melhorias que ainda não foram resolvidos.

Isso não é uma falha do trabalho atual. É a natureza da engenharia de performance. A linha de base se move, o volume de dados dos clientes cresce, e a definição de "rápido o suficiente" muda junto. O valor dessa rodada de otimizações não são apenas os milissegundos economizados. É o processo estabelecido para encontrar e fechar a próxima lacuna.

O que vem a seguir para Ignite UI desempenho na grade

As otimizações neste post representam uma rodada focada de trabalho de performance e não uma declaração final sobre o tema. Várias áreas já estão em andamento, e outras estão sendo ativamente exploradas.

O que já melhorou

O desempenho da virtualização teve melhorias junto com o trabalho de ordenação, agrupamento e filtragem abordado neste post. A virtualização por linhas e colunas é a base que torna viável a renderização de grandes conjuntos de dados. Todas as melhorias ali se somam aos ganhos do pipeline de dados, o que significa que a grade é mais rápida tanto no processamento quanto na renderização dos dados.

O que ainda está sendo trabalhado

A análise de datas continua sendo uma área com espaço conhecido para melhorias. Os resultados de ordenação e ESF para colunas de data e hora são dramaticamente melhores do que antes, mas ainda são mais lentos que as colunas numéricas de maneiras que remontam à forma como as strings de data são analisadas. Um trabalho mais direcionado na camada de análise sintática é o próximo passo lógico.

O tamanho do pacote é um foco contínuo. Uma grade mais rápida que envia mais JavaScript do que o necessário funciona contra si mesma, especialmente para equipes onde o tempo inicial de carregamento é tão importante quanto o desempenho em tempo de execução. Reduzir a pegada da rede sem sacrificar a capacidade é um ato de equilíbrio contínuo.

O refinamento da API de grade continua em paralelo. Não é uma preocupação direta de desempenho, mas está conectada a isso. Uma API mais limpa reduz a área superficial onde caminhos de código sensíveis ao desempenho são invocados de maneiras não intencionais.

O desempenho em tempo de execução de forma mais ampla, incluindo custo de renderização, pressão de detecção de mudanças, resposta à interação sob atualizações de alta frequência, continua sendo uma área aberta a ser explorada. Sem afirmações específicas, mas está no radar.

Compartilhe seu feedback sobre desempenho

Cada melhoria de desempenho eleva a linha de base e as expectativas. O que antes era lento se torna rápido, e novos gargalos eventualmente surgem.

Por isso, valorizamos o feedback útil do uso real. Se você está usando Ignite UI grids em produção e enfrenta problemas de desempenho, abra uma edição no GitHub. Cenários reais e casos reproduzíveis nos ajudam a identificar as próximas oportunidades de melhoria.

Encerramento: Desempenho como Promessa, Não como Ponto Principal

Toda biblioteca de grade lista o desempenho como um recurso. "Gerencia milhões de linhas" aparece em tabelas comparativas junto com outros recursos como uma caixa de seleção, não um compromisso.

Há uma diferença entre uma grade que tecnicamente lida com grandes conjuntos de dados e outra que os gerencia sem fazer os usuários esperarem. Essa diferença não aparece em uma lista de recursos. Ele aparece quando um usuário clica em um cabeçalho de coluna ou abre um diálogo de filtro e recebe uma resposta imediata ou assiste a interface travar.

O trabalho neste post foi motivado por essa distinção. Não por uma exigência de marketing – por um cliente real enfrentando barreiras reais de desempenho, e pelo reconhecimento de que "funciona" e "é rápido" não são a mesma afirmação. A transformada Schwartziana, a pilha de agrupamento iterativa, o pipeline ESF de passagem única – nada disso era óbvio inicialmente, nada garantido funcionaria, e tudo isso exigia medição para justificar.

Performance não é um recurso que você lança e depois segue em frente. É uma obrigação contínua para com os desenvolvedores e usuários finais que dependem desses componentes fazerem trabalho real, em escala real, sem que a interface atrapalhe.

Pretendemos continuar cumprindo isso.

Solicite uma demonstração