如何为大型阵列加速ngFor?

Cor*_*urn 16 angular

我正在开发一个Pokedex网站,感谢现在正在使用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>
Run Code Online (Sandbox Code Playgroud)

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

加载MaterialPokedex.com的时间表

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

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

有没有办法加快这个ngFor?

我正在使用Angular 2 RC1.我正在启用prod模式.我在Chrome 51.0.2704.79米中运行它

Cor*_*urn 20

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

这是我的html的简化版本,只有这个问题的相关部分(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>
Run Code Online (Sandbox Code Playgroud)

超小型结构,绝对清晰:

<div> <!-- column -->
  <div></div> <!-- spacer -->
  <entry *ngFor='...'></entry>
  <div></div> <!-- spacer -->
</div>
Run Code Online (Sandbox Code Playgroud)

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

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

在口袋妖怪列表中可视空间上方和下方的间隔物

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

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

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

实际构建所有<entry>元素的ngFor 看起来像这样:

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

打破这种情况:

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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的发布,现在有一个用于虚拟滚动的Angular Material包.我对我的网站进行了一些更改,并在大约一个小时内完成了虚拟滚动工作.即使有组件回收.我完全建议使用Angular Material进行虚拟滚动.