在 Angular 中渲染大型列表的 3 种方法框架很快,但你的代码很慢

2025-06-08

在 Angular 中渲染大型列表的 3 种方法

框架很快,你的代码很慢

使用 Angular 渲染大型项目列表的可用技术概述

本文最初Giancarlo Buomprisco在Bits and Pieces上发表

2020 年的框架变得更好、更高效、更快速。即便如此,即使对于目前最快的框架来说,在 Web 上渲染大量项目列表而不导致浏览器卡顿仍然很困难。

这是“框架很快,代码很慢”的众多情况之一。

有许多不同的技术可以为用户以非阻塞的方式渲染大量项目。在本文中,我将探讨当前可用的技术,并根据具体的用例选择最佳技术。

虽然本文重点介绍如何使用 Angular 优化渲染,但这些技术实际上也适用于其他框架或简单的 Vanilla Javascript。

框架很快,你的代码很慢

本文详细介绍了我在之前的一篇文章中讨论的一个方面:渲染过多的数据。Angular
应用运行缓慢的主要原因

我们将研究以下技术:

  • 虚拟滚动(使用 Angular CDK)

  • 手动渲染

  • 渐进式渲染


无论你选择哪种实现来渲染长列表,请务必将可复用的 Angular 组件共享到Bit.dev的组件中心。这将节省你重复工作的时间,并使你和你的团队更容易在 Angular 项目中使用经过测试和性能优化的代码。

您可以在我之前的文章中阅读更多相关信息:
使用 Angular 和 Bit 共享组件
*Bit 简介:构建和共享 Angular 组件*blog.bitsrc.io


1.虚拟滚动

虚拟滚动可能是处理大型列表最有效的方法,但有一个限制。得益于Angular CDK和其他插件,它可以很容易地在任何组件中实现。

这个概念很简单,但实现起来并不总是最容易的:

  • 给定一个容器和一个项目列表,只有当项目位于容器的可见边界内时,才会渲染该项目

要使用CDK的Scrolling模块,我们首先需要安装该模块:

npm i @angular/cdk
Enter fullscreen mode Exit fullscreen mode

然后,我们导入模块:

    import { ScrollingModule } from '@angular/cdk/scrolling';

    @NgModule({
     ...
     imports: [ ScrollingModule, ...]
    })
    export class AppModule {}  
Enter fullscreen mode Exit fullscreen mode

我们现在可以使用组件在我们的组件中使用虚拟滚动:

    <cdk-virtual-scroll-viewport itemSize="50">       
     <div *cdkVirtualFor="let item of items">
       {{ item }}
     </div>
    </cdk-virtual-scroll-viewport>
Enter fullscreen mode Exit fullscreen mode

正如你所见,它非常容易使用,而且效果令人印象深刻。该组件可以毫无问题地渲染成千上万个项目。

如果虚拟滚动这么好,而且很容易实现,为什么还要费心探索其他技术呢?这也是我一直想知道的——事实上,原因不止一个。

  • 它的工作方式很大程度上取决于具体实现:单一实现很难管理所有可能的场景。
    例如,我的组件依赖于自动完成字段(由同一团队构建),但不幸的是,它并没有按预期工作。你的项目越复杂,处理起来就越困难

  • 另一个模块,另一大块代码已添加到您的应用程序中

  • 可访问性和可用性:隐藏的项目不会呈现,因此无法搜索。

虚拟滚动在很多情况下都是理想的(当它起作用时):

  • 未定义且可能非常庞大的项目列表(大约超过 5k,但这在很大程度上取决于每个项目的复杂性)

  • 无限滚动项目

2. 手动渲染

我尝试过的加速大量项目列表的选项之一是使用 Angular 的 API 手动渲染,而不是依赖 *ngFor。

我们有一个简单的 ngFor 循环模板:

    <tr 
        *ngFor="let item of data; trackBy: trackById; let isEven = even; let isOdd = odd"
        class="h-12"
        [class.bg-gray-400]="isEven"
        [class.bg-gray-500]="isOdd"
    >
      <td>
        <span class="py-2 px-4">{{ item.id }}</span>
      </td>

      <td>
        <span>{{ item.label }}</span>
      </td>

      <td>
        <a>
          <button class="py-2 px-4 rounded (click)="remove(item)">x</button>
        </a>
      </td>
    </tr>
Enter fullscreen mode Exit fullscreen mode

我正在使用受js-frameworks-benchmark启发的基准来计算 10000 个简单项目的渲染。

第一次基准测试是用简单、常规的 *ngFor 执行的。结果如下:脚本执行耗时 1099 毫秒,渲染耗时 1553 毫秒,绘画耗时 3 毫秒。

通过使用 Angular 的 API,我们可以手动渲染项目。

    <tbody>
      <ng-container #itemsContainer></ng-container>
    </tbody>

    <ng-template #item let-item="item" let-isEven="isEven">
      <tr class="h-12"
          [class.bg-gray-400]="isEven"
          [class.bg-gray-500]="!isEven"
      >
        <td>
          <span class="py-2 px-4">{{ item.id }}</span>
        </td>

        <td>
          <span>{{ item.label }}</span>
        </td>

        <td>
          <a>
            <button class="py-2 px-4 rounded" (click)="remove(item)">x</button>
          </a>
        </td>
      </tr>
    </ng-template>
Enter fullscreen mode Exit fullscreen mode

控制器的代码改变如下:

  • 我们声明我们的模板和容器
    @ViewChild('itemsContainer', { read: ViewContainerRef }) container: ViewContainerRef;
    @ViewChild('item', { read: TemplateRef }) template: TemplateRef<*any*>;
Enter fullscreen mode Exit fullscreen mode
  • 当我们构建数据时,我们还使用ViewContainerRef createEmbeddedView方法对其进行渲染
    private buildData(length: number) {
      const start = this.data.length;
      const end = start + length;

      for (let n = start; n <= end; n++) {
        this.container.createEmbeddedView(this.template, {
          item: {
            id: n,
            label: Math.random()
          },
          isEven: n % 2 === 0
        });
      }
    }
Enter fullscreen mode Exit fullscreen mode

结果显示出适度的改善:

  • 脚本编写耗时 734 毫秒,渲染耗时 1443 毫秒,绘画耗时 2 毫秒

但实际上,它仍然非常慢!点击按钮时,浏览器会冻结几秒钟,给用户带来糟糕的用户体验。

它看起来是这样的(我正在移动鼠标来模拟加载指示器😅):

现在让我们尝试将渐进式渲染手动渲染相结合。

3.渐进式渲染

渐进式渲染的概念很简单,就是逐步渲染一部分项目,并推迟事件循环中其他项目的渲染。这使得浏览器能够平滑、渐进地渲染所有项目。

下面的代码很简单:

  • 我们创建一个每 10 毫秒运行一次的间隔,并同时渲染 500 个项目

  • 当所有项目都已渲染后,根据索引,我们停止间隔并中断循环

    private buildData(length: number) {
      const ITEMS_RENDERED_AT_ONCE = 500;
      const INTERVAL_IN_MS = 10;

      let currentIndex = 0;

      const interval = setInterval(() => {
        const nextIndex = currentIndex + ITEMS_RENDERED_AT_ONCE;

        for (let n = currentIndex; n <= nextIndex ; n++) {
          if (n >= length) {
            clearInterval(interval);
            break;
          }

          const context = {
            item: {
              id: n,
              label: Math.random()
            },
            isEven: n % 2 === 0
          };

          this.container.createEmbeddedView(this.template, context);
        }

        currentIndex += ITEMS_RENDERED_AT_ONCE;
      }, INTERVAL_IN_MS);
Enter fullscreen mode Exit fullscreen mode

请注意,渲染项目的数量和间隔时间完全取决于您的具体情况。例如,如果您的项目非常复杂,那么一次渲染 500 个项目肯定会非常慢。

正如您在下面看到的,统计数据看起来确实更糟:

但用户体验并没有变差。尽管渲染列表所需的时间比以前更长,但用户却感觉不到。我们一次渲染 500 个项目,而且渲染发生在容器边界之外。

当容器改变其大小或滚动位置时,可能会出现一些问题,因此在某些情况下需要缓解这些问题。

让我们看看它是什么样子的:

最后的话

上述技术在某些情况下确实很有用,并且每当虚拟滚动不是最佳选择时我都会使用它们。

话虽如此,但在大多数情况下,使用像 Angular 的 CDK 这样的优秀库进行虚拟滚动绝对是处理大型列表的最佳方式。

如果您需要任何澄清,或者您认为某些内容不清楚或错误,请发表评论!

希望你喜欢这篇文章!如果喜欢,请在MediumTwitterDev上关注我,获取更多关于软件开发、前端、RxJS、Typescript 等的文章!

鏂囩珷鏉ユ簮锛�https://dev.to/angular/3-ways-to-render-large-lists-in-angular-10nl
PREV
深入理解 Angular 12
NEXT
后端开发:2024 年的定义、统计数据和趋势