在Angular中呈现大列表的3种方法

2020年,前端框架变得更好,更高效,更快。但是,即使考虑到这一点,即使对于最快的现有框架,在不冻结浏览器的情况下呈现大型列表仍然是一项艰巨的任务。

这是“框架快速而代码缓慢”的情况之一。 有许多方法可让您显示大量元素,而不会阻止用户与网页的交互。 这篇文章的作者(我们今天将要翻译的译本)希望探索在网页上显示大列表的现有方法,并讨论它们的应用领域。







尽管此材料针对的是Angular,但此处讨论的内容适用于其他使用纯JavaScript编写的框架和项目。特别是,此处将考虑以下呈现大型列表的方法:

  • 虚拟滚动(使用Angular CDK)。
  • 手动渲染。
  • 渐进式渲染。

虚拟滚动


虚拟滚动可能是处理大型列表的最有效方法。是的,他有一些缺点。借助Angular CDK和其他插件,虚拟滚动非常容易在任何组件中实现。

虚拟滚动背后的想法很简单,但并不总是易于实现。最重要的是,我们有一个容器和一个元素列表。仅当元素在容器的可见区域内时才渲染它。

我们将使用scrollingAngular CDK中的模块该模块旨在组织虚拟滚动。为此,您首先需要安装CDK:

npm i @angular/cdk

然后,您需要导入模块:

import { ScrollingModule } from '@angular/cdk/scrolling';
@NgModule({
 ...
 imports: [ ScrollingModule, ...]
})
export class AppModule {}

之后,您可以在组件中使用cdk-virtual-scroll-viewport

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

这是使用此方法组织虚拟滚动的示例项目。

如您所见,标准的Angular机制可以轻松实现令人印象深刻的结果。示例中的组件呈现了数千个元素,没有任何问题。

如果虚拟滚动效果很好,并且易于实现,那么就会出现一个问题,即为什么要研究其他方法来渲染大型列表。我对这个问题也很感兴趣。事实证明,这种情况有几个原因:

  • , , . , . , Autocomplete ( ). , , , , . — .
  • — , .
  • 虚拟滚动会导致列表内容的可访问性和易用性方面存在一些问题。隐藏的元素不会呈现-这意味着屏幕阅读器将无法使用它们,也无法使用标准的浏览器机制在页面上找到它们。

虚拟滚动是在许多情况下的理想选择(前提是它可以工作):

  • 在这种情况下,如果要显示事先不知道大小的列表,或者可能要显示的列表可能很大(根据粗略估计,列表包含超过5000个元素,但这在很大程度上取决于每个元素的复杂性)。
  • 如果您需要组织无休止的滚动。

手动渲染


我尝试加速大量元素输出的列表的一种处理方法是改用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>

为了衡量10,000个简单元素的渲染性能,我使用了一个基于js-frameworks-benchmark的基准

首先,我检查了列表的性能,该列表使用常规循环输出*ngFor结果,脚本花费了1099毫秒,渲染(Rendering)花费了1553毫秒,绘画(Painting)花费了3毫秒。


研究列表的性能,它使用* ngFor来显示输出,

可以使用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>

让我们谈谈控制器代码如何更改。

我们声明了一个模板和容器:

@ViewChild('itemsContainer', { read: ViewContainerRef }) container: ViewContainerRef;
@ViewChild('item', { read: TemplateRef }) template: TemplateRef<any>;

生成数据时,我们使用createEmbeddedView实体方法渲染它们ViewContainerRef

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
    });
  }
}

结果,表征列表性能的指标略有改善。即,执行代码花费了734毫秒,渲染花费了1443毫秒,绘制花费了2毫秒。




实际上,研究使用Angular API输出 True的列表的性能,这意味着该列表仍然非常慢。当您单击适当的按钮时,浏览器“冻结”几秒钟。如果它出现在生产中,用户肯定会不喜欢它。

外观如下所示(这里,我用鼠标模仿了加载指示器)。




现在,列表变慢了,为了改善手动列表渲染,我们将尝试应用渐进式渲染技术。

渐进式渲染


渐进式渲染的想法很简单。它包括逐步渲染元素的子集,从而延迟事件循环中其他元素的渲染。这使浏览器可以显示所有元素,而无需“放慢速度”。

以下实现渐进式列表呈现的代码非常简单:

  • 首先,使用this setInterval,我们每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);

请注意,每次调用要传送的功能时显示的元素数量setInterval以及调用此功能的频率完全取决于具体情况。例如,如果显示的列表中的元素非常复杂,那么一遍中500个这样的元素的输出速度将非常慢。

通过测量该解决方案的性能,我得到的结果看上去比之前收到的结果差。代码执行-907毫秒,渲染-2555毫秒,绘图-16毫秒


研究列表的性能,列表的输出使用渐进式渲染,

但是使用此列表的用户将比以前拥有更多愉悦的感觉。尽管渲染列表所需的时间增加了,但用户不会注意到它。我们一口气渲染了500个元素。同时,在容器边界之外执行渲染。

在此,由于容器在渲染期间更改大小或内容滚动的位置发生更改,可能会出现一些问题。如果发生这种情况,您将不得不处理类似的问题。

这是处理此类列表的样子。


列表很快

摘要


在某些情况下,大型列表的手动和渐进式渲染技术当然很有用。在由于某些原因导致虚拟滚动不适合我的情况下,我使用了它们。

鉴于以上所述,我们可以说,基于良好的库(例如Angular CDK)构建的大多数虚拟滚动是显示大列表的最佳方法。但是,如果由于某种原因无法使用虚拟滚动,则开发人员还有其他可能性。

亲爱的读者们!如何在Web项目中输出大列表?


All Articles