你的应用需要展示一个包含 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 中)渲染的,永远只是当前视口中可见的那寥寥十几个元素。
-
工作原理:
-
它先计算出整个列表的「理论总高度」,撑开滚动条,让其看起来像一个真正的长列表。
-
它只创建并渲染当前视口内可见的 DOM 节点(外加一小部分缓冲区)。
-
当用户滚动时,它会「回收」那些滚出视口的 DOM 节点,并用它们去渲染那些即将滚入视口的、新的数据项。
-
-
Angular 的实现:CDK Virtual Scrolling
Angular 的组件开发工具包(CDK)为我们提供了开箱即用的虚拟滚动解决方案,使用起来难以置信地简单。
你只需要:
-
安装并导入
ScrollingModule(或在你的独立组件中直接imports)。 -
用
<cdk-virtual-scroll-viewport>包裹你的列表。 -
把
@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 中同时存在的,永远只有那么几十个节点。性能问题,迎刃而解。

第三式:返璞归真 —— 分页#
有时候,我们并不需要给用户一个「无限」滚动的幻觉。传统而朴素的分页(Pagination),在很多场景下,依然是最高效、最清晰的解决方案。
-
适用场景:数据表格、搜索引擎结果、商品目录等。在这些场景,用户通常更关心「总共有多少页」、「我现在在第几页」,分页能提供更强的「方位感」。
-
实现:分页的实现,通常需要后端 API 的配合(支持
page和pageSize参数),前端则需要一个分页 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 为我们提供了从宏观到微观的全套解决方案。掌握它们,你才能在这场与海量数据的「对弈」中,做到「纲举目张」 (意指抓住事物的主要环节,就能带动其他环节。比喻抓住要领,带动全面),无论是面对万行数据还是百万数据,都能让你的应用如德芙般纵享丝滑,告别卡顿,赢取用户的赞誉。