如何为大型阵列加速ngFor?

时间:2016-06-02 19:04:08

标签: angular

我正在Pokedex site工作,感谢现在正在使用721口袋妖怪,因为第一次显示所有条目需要很长时间。一旦我加载了所有数据,它似乎需要大约2400毫秒来实际将它们放入DOM中。

这里有问题的ngFor:

<entry *ngFor="let p of (pokedex | filter:search:SelectedVer), let i = index, let last = last"
    [id]="'pokemon-entry-' + p.id"
    [pokemon]="p"
    [language]="SelectedLang"
    (click)="SelectPokemon(p)"></entry>

我在Chrome的开发工具中运行了一个时间轴,并得到了类似的内容:

Timeline of MaterialPokedex.com loading up

我对时间轴没有太多经验,但在我看来,中间有一个太大的块(顶部标有XHR Load (/csv/pokemon_game_indices.csv))。根据时间线,ajax调用本身需要0.02 ms。我假设是什么使这个如此大的块是在ajax请求完成后发生的变化检测。当我拿走我正在构建的模型并将它们放在ngFor使用的pokedex变量中时。我对时间线的理解是,由ngFor添加的721 DOM元素的构建需要大约2.5秒才能完成。

我确实尝试将我的entry组件非组件化为html(该组件确实没有做任何事情),但这似乎并没有以任何明显的方式影响时间。删除我用来过滤列表的管道也不会影响时间。

有没有办法加快这个ngFor?

我正在使用Angular 2 RC1。我正在启用prod模式。我在Chrome 51.0.2704.79 m

中运行此功能

1 个答案:

答案 0 :(得分:19)

简短而甜蜜的回答是“不要遍历整个阵列”,但这对我来说还不够好。我希望它看起来像整个条目列都存在。所以我在上面放了一个spacer,ngFor迭代了数组的一个子部分,下面的一个spacer和一起使得列表看起来像所有元素一直存在。

这是我的html的简化版本,只包含此问题的相关部分(full example on bitbucket):

<div (scroll)="ColScroll($event)">
  <div [style.height]="Math.max(0, Math.max(0, scrollPos - 10) * 132)"></div>
  <entry *ngFor="let p of (base.pokemon | filter:search:SelectedVer:SelectedLang) | justafew:scrollPos" [pokemon]="p"></entry>
  <div [style.height]="Math.max(0,((base.pokemon | filter:search:SelectedVer:SelectedLang).length - scrollPos - 40)) * 132"></div>
</div>

极小的结构,绝对清晰:

<div> <!-- column -->
  <div></div> <!-- spacer -->
  <entry *ngFor='...'></entry>
  <div></div> <!-- spacer -->
</div>

首先,一个非常关键的点:<entry>总是正好120像素高,12像素底部边距总共132个像素的空间。 CSS使这绝对。这适用于我想要选择的任何恒定大小,但我做出特殊的假设,即大小正好是132像素。

简短版本是滚动列时的scrollHeight确定哪些条目实际上应该在屏幕上。如果ngFor实际构建的前10个元素在屏幕外,则第一个可见元素从11开始。我占用4k屏幕并显示40个条目(占用5280像素)以确保整个列看起来已满。然后,所以滚动条看起来正确,我在40个条目下面有一个间隔,以强制div具有适当的可滚动高度。这是一个视觉上正在发生的图像:

Spacer above and below the viewable space in the list of pokemon

以下是控制器中的相关变量和函数(bitbucket):

scrollPos = 0;
...
ColScroll(event: Event) {
  let pos = $(event.target).scrollTop();
  this.scrollPos = Math.floor(pos / 132);
}

它让我在这里使用jQuery,但我已经把它用于别的东西了,我需要一些跨浏览器的东西。 scrollPos保存我应该在屏幕上显示的第一个项目的第一个索引。

实际构建所有<entry>元素的ngFor如下所示:

*ngFor="let p of (base.pokemon | filter:search:SelectedVer:SelectedLang) | justafew:scrollPos"

打破这一点:

base.pokemon是创建每个条目元素所必需的宠物小精灵数据的数组。

... | filter:search:SelectedVer:SelectedLang)用于搜索列表。我把它保留在我的示例中,以表明在我的黑客进场之前你仍然可以使用该列表。

... | justafew:scrollPos是神奇发生的地方。这是完整的过滤器(bitbucket):

import { Pipe, PipeTransform } from '@angular/core';

import { MinPokemon } from '../models/base';

@Pipe({
  name: 'justafew',
  pure: false
})
export class JustAFewPipe implements PipeTransform {
  public transform(value: MinPokemon[], start: number): MinPokemon[] {
    return value.slice(Math.max(0, start - 10), start + 30);
  }
}

scrollPos作为start参数传入。例如,如果我在列中向下滚动了13200像素,则scrollPos将设置为100(请参阅上面控制器中的滚动事件)。这将切割数组,使其返回元素90到130.我想稍微溢出屏幕以确保快速滚动不会导致可见的空白区域(疯狂的快速滚动可能仍然显示它但你移动得那么快很容易认为浏览器没有快速渲染,所以我让它滑动)。我使用Math.max因此我不会使用负数进行切片,例如当我位于列表顶部并且scrollPos为0时。

现在的垫片。他们保持滚动条诚实。我绑定他们的[style.height]并使用一点数学来使这些间隔物占用所需的空间。当我向下滚动时,顶部垫片长得更高,底部垫片收缩的量完全相同,因此柱子的高度始终相同。当我向后滚动时,数学运算正好相反:顶部收缩,底部增长。底部间隔使用与ngFor完全相同的过滤逻辑,这样如果我运行一个返回100而不是721口袋妖怪的搜索,它会调整到100个条目的高度。使用scrollPos - 10的第一个间隔因为justafew滤镜返回10.出于同样的原因,底部间隔使用scrollPos - 30,因为这是justafew返回的数量。

我知道它看起来像很多活动部件,但它们都很简单快捷。不幸的是,在这个地方有很多“魔术数字”相互依赖,但考虑到性能的提高和可靠性,这让我完全展示了我让它滑动的整个列表。也许有一天我会制作一个组件或指令,将它全部放在一个可配置的地方。

更新:2年半左右,Angular 7发布there's now a Angular Material package for virtual scrolling。我将a few changes发送到了我的网站,并在大约一个小时内完成了虚拟滚动工作。即使有组件回收。我完全建议使用Angular Material进行虚拟滚动。