【问题标题】:How can I speed up ngFor for a large array?如何为大型阵列加速 ngFor?
【发布时间】:2018-10-31 12:37:05
【问题描述】:

我正在处理Pokedex site,并且由于现在有 721 个 Pokemon,ngFor 第一次显示所有条目需要很长时间。一旦我加载了所有数据,似乎需要大约 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 的开发工具中运行了一个时间线,得到的结果如下所示:

我对时间线没有太多经验,但在我看来,中间有一个太大的块(顶部标记为XHR Load (/csv/pokemon_game_indices.csv))。根据时间线,ajax 调用本身需要 0.02 毫秒。我假设使这个块如此大的原因是在 ajax 请求完成后发生的更改检测。那时我将我一直在构建的模型放入 ngFor 上面使用的 pokedex 变量中。我对时间线的理解是,ngFor要添加的721个DOM元素的构建大约需要2.5s才能完成。

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

有没有办法加快这个 ngFor?

我正在使用 Angular 2 RC1。我正在启用产品模式。我在 Chrome 51.0.2704.79 m 中运行它

【问题讨论】:

  • 我不确定您是如何显示数据的,但没有必要在屏幕上显示 721 个内容。您只能看到其中的一小部分。你不能只分页数据以便更容易查看吗?所以你只会显示 10 - 100 个口袋妖怪而不是 721 个。这样会更快。
  • this 应该可以帮助您进行分页
  • 最好的优化是什么都不做。如前所述,只渲染需要一次显示的内容。
  • 您的网站要快得多。你是怎么解决这个问题的?
  • 双重解决方案。首先,与这个问题无关,我在请求什么数据时最好分开。我没有预先给你整个 pokedex,所以我将初始下载从大约 12 MB 减少到 35 KB。它只请求列表、过滤器和其他一些东西所需的基本数据。第二个解决方案与这个问题 100% 相关,我将把它作为答案发布。

标签: angular


【解决方案1】:

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

这是我的 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>

首先,非常关键的一点:&lt;entry&gt; 的高度始终为 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);
}

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

实际构建所有 &lt;entry&gt; 元素的 ngFor 如下所示:

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

分解:

base.pokemon 是创建每个条目元素所需的口袋妖怪数据数组。

... | filter:search:SelectedVer:SelectedLang) 用于搜索列表。我将它留在我的示例中,以表明您仍然可以在我的 hack 发挥作用之前使用该列表。

... | 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 pokemon 的搜索,它会调整为 100 个条目的高度。第一个间隔使用scrollPos - 10,因为justafew 过滤器返回10。出于同样的原因,底部间隔使用scrollPos - 30,因为这是justafew 返回的数量。

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

更新:大约 2 1/2 年后,Angular 7 发布 there's now a Angular Material package for virtual scrolling。我在我的网站上创建了a few changes,并在大约一个小时内完成了虚拟滚动。即使是组件回收。我强烈推荐使用 Angular Material 进行虚拟滚动。

【讨论】:

  • 我,我必须使用 [style.height.px] 而不是 [style.height] 才能让它工作。无论如何,很棒的解决方案!
  • 另外,当在模板中使用Math 时,Angular 会抛出错误。我必须在组件中声明 getter 来解决这个问题。感谢您提供简单而优雅的解决方案!
  • @vanelizarov 对,我应该提到这一点。在包含该列的组件中,我有 public Math: Math = Math; 使其可用。
  • 但是,当您一次最多只渲染 5320 像素时,您将如何向下滚动 13200 像素?
  • 感谢页眉和页脚,13200 像素始终可以滚动浏览。只有 5320 像素有内容,并且内容窗口会随着您滚动而移动。
猜你喜欢
  • 2023-03-05
  • 1970-01-01
  • 2019-08-10
  • 2018-07-09
  • 2020-09-24
  • 2011-07-24
  • 1970-01-01
  • 2014-07-17
  • 1970-01-01
相关资源
最近更新 更多