Virtualização do componente do Razor ASP.NET Core

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Este artigo explica como usar a virtualização de componentes em aplicativos Blazor do ASP.NET Core.

Virtualization

Melhore o desempenho percebido da renderização de componentes usando o Blazorsuporte interno de virtualização da estrutura com o componente Virtualize<TItem>. A virtualização é uma técnica para limitar a renderização da interface do usuário apenas às partes que estão visíveis no momento. Por exemplo, a virtualização é útil quando o aplicativo deve renderizar uma longa lista de itens e apenas um subconjunto de itens é necessário para ficar visível a qualquer momento.

Usar o componente Virtualize<TItem> quando:

  • For renderizar um conjunto de itens de dados em um loop.
  • A maioria dos itens não estiver visível devido à rolagem.
  • Os itens renderizados tiverem o mesmo tamanho.

Quando o usuário rola para um ponto arbitrário na lista de itens do componente Virtualize<TItem>, o componente calcula os itens visíveis a serem mostrados. Itens não vistos não são renderizados.

Sem virtualização, uma lista típica pode usar um loop foreach C# para renderizar cada item em uma lista. No exemplo a seguir:

  • allFlights é uma coleção de voos de avião.
  • O componente FlightSummary exibe detalhes sobre cada voo.
  • O atributo de diretiva @key preserva a relação de cada componente FlightSummary com seu voo renderizado pelo FlightId do voo.
<div style="height:500px;overflow-y:scroll">
    @foreach (var flight in allFlights)
    {
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    }
</div>

Se a coleção contiver milhares de voos, a renderização dos voos levará muito tempo e os usuários enfrentarão um atraso perceptível na interface do usuário. A maioria dos voos não é vista porque eles ficam fora da altura do elemento <div>.

Em vez de renderizar toda a lista de voos de uma só vez, substitua o loop foreach no exemplo anterior pelo componente Virtualize<TItem>:

  • Especifique allFlights como uma origem de item fixa para Virtualize<TItem>.Items. Somente os voos visíveis no momento são renderizados pelo componente Virtualize<TItem>.

    Se uma coleção não genérica fornecer os itens, por exemplo, uma coleção de DataRow, siga as orientações da seção representante do provedor de itens para fornecer os itens.

  • Especifique um contexto para cada voo com o parâmetro Context. No exemplo a seguir, flight é usado como o contexto, que fornece acesso aos membros de cada voo.

<div style="height:500px;overflow-y:scroll">
    <Virtualize Items="allFlights" Context="flight">
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    </Virtualize>
</div>

Se um contexto não for especificado com o parâmetro Context, use o valor de context no modelo de conteúdo do item para acessar os membros de cada voo:

<div style="height:500px;overflow-y:scroll">
    <Virtualize Items="allFlights">
        <FlightSummary @key="context.FlightId" Details="@context.Summary" />
    </Virtualize>
</div>

O componente Virtualize<TItem>:

  • Calcula o número de itens a serem renderizados com base na altura do contêiner e no tamanho dos itens renderizados.
  • Recalcula e renderiza os itens novamente à medida que o usuário rola.
  • Somente busca a fatia de registros de uma API externa que corresponda à região visível no momento, incluindo o pré-carregamento, quando ItemsProvider é usado em vez de Items (consulte a seção Representante do provedor de itens).

O conteúdo do item para o componente Virtualize<TItem> pode incluir:

  • HTML sem formatação e código Razor, como mostra o exemplo anterior.
  • Um ou mais componentes Razor.
  • Uma combinação de HTML/componentes Razor e Razor.

Delegado do provedor de itens

Se você não quiser carregar todos os itens na memória ou se a coleção não for um ICollection<T> genérico, você poderá especificar um método delegado do provedor de itens para o parâmetro do componente Virtualize<TItem>.ItemsProvider que recupera de forma assíncrona os itens solicitados sob demanda. No exemplo a seguir, o método LoadEmployees fornece os itens para o componente Virtualize<TItem>:

<Virtualize Context="employee" ItemsProvider="LoadEmployees">
    <p>
        @employee.FirstName @employee.LastName has the 
        job title of @employee.JobTitle.
    </p>
</Virtualize>

O provedor de itens recebe um ItemsProviderRequest, que especifica o número necessário de itens começando em um índice inicial específico. Em seguida, o provedor de itens recupera os itens solicitados de um banco de dados ou outro serviço e os retorna como um ItemsProviderResult<TItem>, juntamente com uma contagem do total de itens. O provedor de itens pode optar por recuperar os itens com cada solicitação ou armazená-los em cache para que eles estejam prontamente disponíveis.

Um componente Virtualize<TItem> só pode aceitar uma fonte de item de seus parâmetros, portanto, não tente usar simultaneamente um provedor de itens e atribuir uma coleção a Items. Se ambos forem atribuídos, um InvalidOperationException será gerado quando os parâmetros do componente forem definidos em runtime.

O exemplo a seguir carrega funcionários de um EmployeeService (não mostrado):

private async ValueTask<ItemsProviderResult<Employee>> LoadEmployees(
    ItemsProviderRequest request)
{
    var numEmployees = Math.Min(request.Count, totalEmployees - request.StartIndex);
    var employees = await EmployeesService.GetEmployeesAsync(request.StartIndex, 
        numEmployees, request.CancellationToken);

    return new ItemsProviderResult<Employee>(employees, totalEmployees);
}

No exemplo a seguir, uma coleção de DataRow é uma coleção não genérica, portanto, um delegado do provedor de itens é usado para virtualização:

<Virtualize Context="row" ItemsProvider="GetRows">
    ...
</Virtualize>

@code{
    ...

    private ValueTask<ItemsProviderResult<DataRow>> GetRows(ItemsProviderRequest request)
    {
        return new(new ItemsProviderResult<DataRow>(
            dataTable.Rows.OfType<DataRow>().Skip(request.StartIndex).Take(request.Count),
            dataTable.Rows.Count));
    }
}

Virtualize<TItem>.RefreshDataAsync instrui o componente a solicitar dados novamente de seu ItemsProvider. Isso é útil quando os dados externos são alterados. Geralmente, não é necessário chamar RefreshDataAsync ao usar Items.

RefreshDataAsync atualiza os dados de um componente Virtualize<TItem> sem gerar uma nova renderização. Se RefreshDataAsync for invocado de um manipulador de eventos Blazor ou de um método de ciclo de vida do componente, o disparo de uma renderização não será necessário porque ela será disparada automaticamente no final do manipulador de eventos ou do método de ciclo de vida. Se RefreshDataAsync for disparado separadamente de uma tarefa ou evento em segundo plano, como no delegado ForecastUpdated a seguir, chame StateHasChanged para atualizar a interface do usuário no final da tarefa ou evento em segundo plano:

<Virtualize ... @ref="virtualizeComponent">
    ...
</Virtualize>

...

private Virtualize<FetchData>? virtualizeComponent;

protected override void OnInitialized()
{
    WeatherForecastSource.ForecastUpdated += async () => 
    {
        await InvokeAsync(async () =>
        {
            await virtualizeComponent?.RefreshDataAsync();
            StateHasChanged();
        });
    });
}

No exemplo anterior:

  • RefreshDataAsync é chamado primeiro para obter novos dados para o componente Virtualize<TItem>.
  • StateHasChanged é chamado para renderizar novamente o componente.

Espaço reservado

Como a solicitação de itens de uma fonte de dados remota pode levar algum tempo, você tem a opção de renderizar um espaço reservado com o conteúdo do item:

  • Use um Placeholder (<Placeholder>...</Placeholder>) para exibir conteúdo até que os dados do item sejam disponibilizados.
  • Use Virtualize<TItem>.ItemContent para definir o modelo de item para a lista.
<Virtualize Context="employee" ItemsProvider="LoadEmployees">
    <ItemContent>
        <p>
            @employee.FirstName @employee.LastName has the 
            job title of @employee.JobTitle.
        </p>
    </ItemContent>
    <Placeholder>
        <p>
            Loading&hellip;
        </p>
    </Placeholder>
</Virtualize>

Conteúdo vazio

Use o parâmetro EmptyContent para fornecer conteúdo quando o componente tiver sido carregado e Items estiver vazio ou ItemsProviderResult<TItem>.TotalItemCount for zero.

EmptyContent.razor:

@page "/empty-content"

<PageTitle>Empty Content</PageTitle>

<h1>Empty Content Example</h1>

<Virtualize Items="@stringList">
    <ItemContent>
        <p>
            @context
        </p>
    </ItemContent>
    <EmptyContent>
        <p>
            There are no strings to display.
        </p>
    </EmptyContent>
</Virtualize>

@code {
    private List<string>? stringList;

    protected override void OnInitialized() => stringList ??= new();
}

Altere o método OnInitialized lambda para ver as cadeias de caracteres de exibição do componente:

protected override void OnInitialized() =>
    stringList ??= new() { "Here's a string!", "Here's another string!" };

Tamanho do item

A altura de cada item em pixels pode ser definida com Virtualize<TItem>.ItemSize (padrão: 50). O exemplo a seguir altera a altura de cada item do padrão de 50 pixels para 25 pixels:

<Virtualize Context="employee" Items="employees" ItemSize="25">
    ...
</Virtualize>

Por padrão, o componente Virtualize<TItem> mede o tamanho de renderização (altura) de itens individuais após a renderização inicial. Use ItemSize para fornecer um tamanho exato de item com antecedência para ajudar no desempenho de renderização inicial preciso e para garantir a posição de rolagem correta para recarregamentos de página. Se o padrão ItemSize fizer com que alguns itens sejam renderizados fora da exibição visível no momento, uma segunda renderização será disparada. Para manter corretamente a posição de rolagem do navegador em uma lista virtualizada, a renderização inicial deve estar correta. Caso contrário, os usuários poderão exibir os itens errados.

Contagem de overscan

Virtualize<TItem>.OverscanCount determina quantos itens adicionais são renderizados antes e depois da região visível. Essa configuração ajuda a reduzir a frequência de renderização durante a rolagem. No entanto, valores mais altos resultam em mais elementos renderizados na página (padrão: 3). O exemplo a seguir altera a contagem de overscan do padrão de três itens para quatro itens:

<Virtualize Context="employee" Items="employees" OverscanCount="4">
    ...
</Virtualize>

Alterações de estado

Ao fazer alterações em itens renderizados pelo componente Virtualize<TItem>, chame StateHasChanged para forçar a reavaliação e nova renderização do componente. Para saber mais, consulte Renderização de componentes de Razor no ASP.NET Core.

Suporte à rolagem de teclado

Para permitir que os usuários rolem o conteúdo virtualizado usando o teclado, verifique se os elementos virtualizados ou o próprio contêiner de rolagem são focalizáveis. Se você não executar essa etapa, a rolagem de teclado não funcionará em navegadores baseados em Chromium.

Por exemplo, você pode usar um atributo tabindex no contêiner de rolagem:

<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <Virtualize Items="allFlights">
        <div class="flight-info">...</div>
    </Virtualize>
</div>

Para saber mais sobre o significado do valor tabindex-1, 0ou outros valores, consulte tabindex (documentação do MDN).

Estilos avançados e detecção de rolagem

O componente Virtualize<TItem> foi projetado apenas para dar suporte a mecanismos de layout de elemento específicos. Para entender quais layouts de elemento funcionam corretamente, o seguinte explica como Virtualize detecta quais elementos devem estar visíveis para exibição no local correto.

Se o código-fonte for semelhante ao seguinte:

<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <Virtualize Items="allFlights" ItemSize="100">
        <div class="flight-info">Flight @context.Id</div>
    </Virtualize>
</div>

Em runtime, o componente Virtualize<TItem> renderiza uma estrutura DOM semelhante à seguinte:

<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <div style="height:1100px"></div>
    <div class="flight-info">Flight 12</div>
    <div class="flight-info">Flight 13</div>
    <div class="flight-info">Flight 14</div>
    <div class="flight-info">Flight 15</div>
    <div class="flight-info">Flight 16</div>
    <div style="height:3400px"></div>
</div>

O número real de linhas renderizadas e o tamanho dos espaçadores variam de acordo com o estilo e o tamanho da coleção Items. No entanto, observe que há elementos espaçadores div injetados antes e depois do conteúdo. Isso tem duas finalidades:

  • Fornecer um deslocamento antes e depois do conteúdo, fazendo com que os itens visíveis no momento apareçam no local correto no intervalo de rolagem e no próprio intervalo de rolagem para representar o tamanho total de todo o conteúdo.
  • Detectar quando o usuário está rolando além do intervalo visível atual, o que significa que diferentes conteúdos devem ser renderizados.

Observação

Para saber como controlar a marca de elemento HTML do espaçador, consulte a seção Controlar o nome da marca de elemento do espaçador mais adiante neste artigo.

Os elementos do espaçador usam internamente um Observador de Interseção para receber notificação quando estão ficando visíveis. Virtualize depende do recebimento desses eventos.

Virtualize funciona sob as seguintes condições:

  • Todos os itens de conteúdo renderizados, incluindo conteúdo de espaço reservado, têm altura idêntica. Isso possibilita calcular qual conteúdo corresponde a uma determinada posição de rolagem sem primeiro buscar cada item de dados e renderizar os dados em um elemento DOM.

  • Os espaçadores e as linhas de conteúdo são renderizados em uma única pilha vertical com cada item preenchendo toda a largura horizontal. Geralmente, esse é o padrão. Em casos típicos com elementos div, Virtualize funciona por padrão. Se você estiver usando o CSS para criar um layout mais avançado, tenha em mente os seguintes requisitos:

    • O estilo de contêiner de rolagem requer um display com qualquer um dos seguintes valores:
      • block (o padrão para um div).
      • table-row-group (o padrão para um tbody).
      • flex com flex-direction definido como column. Certifique-se de que os filhos imediatos do componente Virtualize<TItem> não sejam reduzidos sob regras flexíveis. Por exemplo, adicione .mycontainer > div { flex-shrink: 0 }.
    • O estilo de linha de conteúdo requer um display com um dos seguintes valores:
      • block (o padrão para um div).
      • table-row (o padrão para um tr).
    • Não use CSS para interferir no layout dos elementos do espaçador. Por padrão, os elementos do espaçador têm um valor display de block, exceto se o pai for um grupo de linhas de tabela, nesse caso, eles assumem table-row como padrão. Não tente influenciar a largura ou a altura do elemento do espaçador, inclusive fazendo com que eles tenham uma borda ou pseudo-elementos content.

Qualquer abordagem que impeça que os espaçadores e elementos de conteúdo sejam renderizados como uma única pilha vertical ou faça com que os itens de conteúdo variem de altura, impede o funcionamento correto do componente Virtualize<TItem>.

Virtualização no nível da raiz

O componente Virtualize<TItem> dá suporte ao uso do documento em si como a raiz de rolagem, como uma alternativa para ter algum outro elemento com overflow-y: scroll. No exemplo a seguir, os elementos <html> ou <body> são estilizados em um componente com overflow-y: scroll:

<HeadContent>
    <style>
        html, body { overflow-y: scroll }
    </style>
</HeadContent>

O componente Virtualize<TItem> dá suporte ao uso do documento em si como a raiz de rolagem, como uma alternativa para ter algum outro elemento com overflow-y: scroll. Ao usar o documento como a raiz de rolagem, evite estilizar os elementos <html> ou <body> com overflow-y: scroll porque faz com que o observador de interseção trate a altura rolável completa da página como a região visível, em vez de apenas o visor de janela.

Você pode reproduzir esse problema criando uma grande lista virtualizada (por exemplo, 100.000 itens) e tentando usar o documento como a raiz de rolagem com html { overflow-y: scroll } nos estilos CSS da página. Embora possa funcionar corretamente às vezes, o navegador tenta renderizar todos os 100.000 itens pelo menos uma vez no início da renderização, o que pode causar um bloqueio na guia do navegador.

Para contornar esse problema antes do lançamento do .NET 7, evite estilizar elementos <html>/<body> com overflow-y: scroll ou adote uma abordagem alternativa. No exemplo a seguir, a altura do elemento <html> é definida como pouco mais de 100% da altura do visor:

<HeadContent>
    <style>
        html { min-height: calc(100vh + 0.3px) }
    </style>
</HeadContent>

O componente Virtualize<TItem> dá suporte ao uso do documento em si como a raiz de rolagem, como uma alternativa para ter algum outro elemento com overflow-y: scroll. Ao usar o documento como a raiz de rolagem, evite estilizar os elementos <html> ou <body> com overflow-y: scroll porque faz com que a altura rolável completa da página seja tratada como a região visível, em vez de apenas o visor de janela.

Você pode reproduzir esse problema criando uma grande lista virtualizada (por exemplo, 100.000 itens) e tentando usar o documento como a raiz de rolagem com html { overflow-y: scroll } nos estilos CSS da página. Embora possa funcionar corretamente às vezes, o navegador tenta renderizar todos os 100.000 itens pelo menos uma vez no início da renderização, o que pode causar um bloqueio na guia do navegador.

Para contornar esse problema antes do lançamento do .NET 7, evite estilizar elementos <html>/<body> com overflow-y: scroll ou adote uma abordagem alternativa. No exemplo a seguir, a altura do elemento <html> é definida como pouco mais de 100% da altura do visor:

<style>
    html { min-height: calc(100vh + 0.3px) }
</style>

Controlar o nome da tag de elemento do espaçador

Se o componente Virtualize<TItem> for colocado dentro de um elemento que requer um nome de tag filho específico, SpacerElement permite que você obtenha ou defina o nome da tag do espaçador de virtualização. O valor padrão é div. Para o exemplo a seguir, o componente Virtualize<TItem> é renderizado dentro de um elemento do corpo da tabela (tbody), portanto, o elemento filho apropriado para uma linha de tabela (tr) é definido como o espaçador.

VirtualizedTable.razor:

@page "/virtualized-table"

<PageTitle>Virtualized Table</PageTitle>

<HeadContent>
    <style>
        html, body {
            overflow-y: scroll
        }
    </style>
</HeadContent>

<h1>Virtualized Table Example</h1>

<table id="virtualized-table">
    <thead style="position: sticky; top: 0; background-color: silver">
        <tr>
            <th>Item</th>
            <th>Another column</th>
        </tr>
    </thead>
    <tbody>
        <Virtualize Items="fixedItems" ItemSize="30" SpacerElement="tr">
            <tr @key="context" style="height: 30px;" id="row-@context">
                <td>Item @context</td>
                <td>Another value</td>
            </tr>
        </Virtualize>
    </tbody>
</table>

@code {
    private List<int> fixedItems = Enumerable.Range(0, 1000).ToList();
}

No exemplo anterior, a raiz do documento é usada como o contêiner de rolagem, portanto, os elementos html e body são estilizados com overflow-y: scroll. Para obter mais informações, consulte os seguintes recursos: