你的应用需要展示一个包含 10000 条联系人的通讯录。一个天真的想法是,直接用 @for 循环,把这 10000 条数据全部渲染成 DOM 节点。

这无异于打印一本一万页的电话黄页,然后「啪」的一声摔在用户面前。

结果可想而知:浏览器会因为需要同时渲染和管理成千上万个 DOM 元素而「当场去世」 —— 内存爆炸、页面卡死、用户愤怒地关闭标签页。渲染大型列表,是所有前端框架都会面临的经典性能挑战。今天,雪狼就传授你几招「化繁为简」的绝学,让你能优雅地驯服这头「性能巨兽」。

第一式:入门心法 track —— 每一个 @for 的「标配」#

这是基础,是内功,是你在使用 @for 时,必须养成的「肌肉记忆」。

  • 痛苦之源:如果没有 track,每当你的列表数据发生任何一丁点变化(哪怕只是调换了两个元素的顺序),Angular 都会简单粗暴地销毁所有的 DOM 元素,然后再重新创建它们。

  • 解救之道:提供一个 track 表达式,告诉 Angular 如何「识别」每一个列表项。

    
    @for (item of items; track item.id) {
    
      <div>{{ item.name }}</div>
    
    }

    track 表达式中,item.id 应该返回一个唯一且稳定的标识符。

  • 效果:现在,Angular 成了一位「火眼金睛」的管理者。当列表更新时,它能精确地知道哪些是新来的,哪些是离开的,哪些只是换了个位置。它只会对真正发生变化的 DOM 进行操作,大大减少了不必要的 DOM 销毁和创建。

请记住:任何使用 @for 循环的列表,都必须带上 track 这是 Angular v17+ 的强制要求,也是性能的「本分」。

第二式:黑科技「空间折叠」 —— 虚拟滚动#

track 能解决数据更新时的卡顿,但它无法解决「初始渲染10000个 DOM」的性能原罪。要解决这个问题,我们需要祭出真正的「黑科技」 —— 虚拟滚动 (Virtual Scrolling)

  • 核心思想:障眼法。我们欺骗用户的眼睛,让他们以为自己正在滚动一个万行列表,但实际上,我们在屏幕上(DOM 中)渲染的,永远只是当前视口中可见的那寥寥十几个元素。

  • 工作原理

    1. 它先计算出整个列表的「理论总高度」,撑开滚动条,让其看起来像一个真正的长列表。

    2. 它只创建并渲染当前视口内可见的 DOM 节点(外加一小部分缓冲区)。

    3. 当用户滚动时,它会「回收」那些滚出视口的 DOM 节点,并用它们去渲染那些即将滚入视口的、新的数据项。

  • Angular 的实现:CDK Virtual Scrolling

    Angular 的组件开发工具包(CDK)为我们提供了开箱即用的虚拟滚动解决方案,使用起来难以置信地简单。

    你只需要:

    1. 安装并导入 ScrollingModule(或在你的独立组件中直接 imports)。

    2. <cdk-virtual-scroll-viewport> 包裹你的列表。

    3. @for 循环放在 cdk-virtual-scroll-viewport 内部。

    
    <cdk-virtual-scroll-viewport itemSize="50" class="long-list-container">
    
      @for (item of veryLongList; track item.id) {
    
        <div class="list-item">
    
          {{ item.name }}
    
        </div>
    
      }
    
    </cdk-virtual-scroll-viewport>
    • itemSize="50":你在告诉虚拟滚动器,你的每个列表项都是固定高度50px。这能让它进行最高效的计算。(它也支持动态高度,但会复杂一些)。

通过虚拟滚动,无论你的列表是一万行还是一百万行,在 DOM 中同时存在的,永远只有那么几十个节点。性能问题,迎刃而解。

文生图:一个用户正在滚动手机屏幕上的一个长列表。屏幕被一个放大镜聚焦,放大镜中显示,DOM树里其实只有十几个 <div> 元素在上下循环复用,而滚动条却非常长。风格:概念清晰的技术图解。

第三式:返璞归真 —— 分页#

有时候,我们并不需要给用户一个「无限」滚动的幻觉。传统而朴素的分页(Pagination),在很多场景下,依然是最高效、最清晰的解决方案。

  • 适用场景:数据表格、搜索引擎结果、商品目录等。在这些场景,用户通常更关心「总共有多少页」、「我现在在第几页」,分页能提供更强的「方位感」。

  • 实现:分页的实现,通常需要后端 API 的配合(支持 pagepageSize 参数),前端则需要一个分页 UI 组件(比如 Angular Material 的 MatPaginator)和相应的状态来管理当前页码。

何时用虚拟滚动,何时用分页?

  • 追求「沉浸式」、「无尽」的浏览体验(如社交动态、新闻 Feed),用虚拟滚动

  • 需要结构化、有界限的数据集导航,用分页

究极进化:@defer 延迟加载块#

在 Angular v17 中,我们又多了一件神兵利器:@defer。它可以将性能优化带到更精细的维度。

  • 新问题:假设你的列表每一项,内部都包含一个需要复杂计算或加载很多子组件的「重量级」组件(比如一个复杂的图表)。即使使用了虚拟滚动,在滚入视口的那一刻,创建这个「重量级」组件依然可能造成卡顿。

  • @defer 的妙用:你可以用 @defer 将这个「重量级」组件的渲染,再次「延迟」。

    
    @for (item of items; track item.id) {
    
      <h2>{{ item.title }}</h2>
    
      @defer (on viewport) {
    
        <app-heavy-chart [data]="item.chartData"></app-heavy-chart>
    
      } @placeholder {
    
        <div class="chart-placeholder">图表加载中...</div>
    
      }
    
    }
  • 效果:现在,当列表项滚入视口时,用户会先看到标题、摘要和一个占位符。只有当这个占位符本身也进入视口时,@defer 块才会被触发,开始真正地渲染那个「重量级」的图表组件。这为你争取了宝贵的几十甚至几百毫秒,让滚动体验如德芙般丝滑。

结语#

优化大型列表的渲染,是一场与「DOM」的博弈,也是一场对用户体验的极致追求。其核心思想,并非蛮力硬抗,而是「节制」与「智慧」的结合:永远不要去渲染用户看不到的东西,永远不要去做不必要的计算

track 的「基本功」筑基,到虚拟滚动的「空间折叠」黑科技,再到 @defer 的「延迟微操」精进,Angular 为我们提供了从宏观到微观的全套解决方案。掌握它们,你才能在这场与海量数据的「对弈」中,做到「纲举目张」 (意指抓住事物的主要环节,就能带动其他环节。比喻抓住要领,带动全面),无论是面对万行数据还是百万数据,都能让你的应用如德芙般纵享丝滑,告别卡顿,赢取用户的赞誉。