默认变更检测:是效率助手,还是「焦虑过度」的驾驶员?#

挖坑现场:你的应用刚起步,组件不多,一切都如丝般顺滑。你自然不会去关心「变更检测策略」这种「高级」玩意儿。

坠坑表现:随着应用日益复杂,组件数量上百,页面上还有一些实时数据在跳动。你渐渐发现,哪怕只是在输入框里打个字,整个应用都会感到一丝丝的「粘滞感」。

病因分析:这是 Angular 默认的、基于 Zone.js 的变更检测策略(CheckAlways)的「锅」。它就像一个「焦虑过度」的新手司机,路上任何一点风吹草动(任何异步事件),都会让他把车里所有的仪表盘、后视镜、指示灯全部重新检查一遍。当「仪表盘」(组件)成百上千时,这一遍检查下来,时间就耗费在路上了。

逃生路线

  1. 传统解法:OnPush 策略。将组件的变更检测策略设为 OnPush,并配合不可变数据和 async 管道使用。这能极大地减少不必要的检查,是 Zone.js 时代最核心的性能优化手段。

  2. 现代解法:拥抱 Signals。在新的 Signal-based 组件中,这个「天坑」被从根本上填平了。Signal 的更新是细粒度的,它会直接通知依赖它的视图进行更新,完全绕开了「从上到下」的全局检查。可以说,Signals 是比 OnPush 更彻底、更自然的性能优化方案

99%的情况下,你都不需要 ngDoCheck?警惕这颗「大力出奇迹」的恐慌按钮!#

挖坑现场:你用了 OnPush,但发现当传入对象(@Input())的内部属性变化时,视图不更新了。情急之下,你翻到了 ngDoCheck 这个生命周期钩子,在里面手动比对新旧值的差异,然后手动触发更新。

坠坑表现ngDoCheck 里的代码被执行的频率高到令人发指!它在每一次变更检测周期中都会被调用,无论你的组件输入是否真的变化。如果你在里面放了复杂的深比对逻辑,其性能损耗甚至比默认策略还要恐怖!

逃生路线99% 的情况下,你都不需要 ngDoCheck

  • OnPush 的世界里,正确的做法是使用不可变数据。当数据变化时,创建一个新的对象引用。

  • 在现代的 Signal-based 组件中,这个问题很大程度上自然消失了。因为你可以直接更新嵌套在 signal 中的状态,而 computed 信号能够智能地只在真正依赖的值变化时才重新计算,无需手动比对。

你的 @for 循环有「身份证」吗?别让「脸盲」的健忘症保安拖慢你的列表!#

挖坑现场:在 *ngFor 时代,trackBy 是一个「建议」使用的优化项,很多开发者会忘记它。

坠坑表现:当一个长列表的数据发生更新时(哪怕只是顺序变化),整个列表的 DOM 元素被全部销毁,再全部重建,引发剧烈的性能抖动。

病因分析:没有 track,Angular 就像一个「脸盲症」晚期的健忘保安。他不认识任何一个「老朋友」,只好把所有人都赶出去,再挨个重新检查、放行。这种 DOM 的大规模销毁和重建,是性能的巨大杀手。

文生图:一个夜店门口,一位“脸盲”的保安(无track)正在粗暴地把所有客人(DOM元素)都推出去,让他们重新排队。而另一边,一位戴着眼镜、拿着名单的“精明”保安(有track),正高效地让客人凭ID入场。风格:对比鲜明的卡通漫画。

逃生路线拥抱新的 @for 语法,它「强制」你变好!

在 Angular v17+ 的新版内置控制流中,@for 循环强制要求你必须提供一个 track 表达式。

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

这个看似「霸道」的规定,实则是框架的「良苦用心」。它从语法层面,彻底杜绝了因忘记 trackBy 而导致的性能问题,保证了列表渲染的高效。track 就像是给保安配备了「人脸识别系统」,让他能精准地进行管理。

模板里的「人生哲学家」:为什么复杂函数调用是性能杀手?#

挖坑现场:你的模板里出现了这样的代码:<div>{{ getComplexSummary(item) }}</div>,而 getComplexSummary 是一个需要循环、过滤、计算才能得出结果的函数。

坠坑表现:你在输入框里打字,发现每敲一个字母,页面都会有肉眼可见的延迟。

病因分析:模板里的任何函数调用,都会在每一次变更检测周期中被重新执行。你相当于在组件里安插了一位「人生哲学家」,每次路过(变更检测),你都要问他一遍「人生的意义是什么?」,然后逼着他把所有哲学著作重新读一遍再给你答案。

逃生路线让模板回归纯粹,计算交给「专家」!

  1. 首选(现代解法):使用 computed 信号。

    将复杂的计算逻辑,封装在一个 computed 信号里。

    
    class SomeClass {
    
        // component.ts
    
        item = signal({ ... }); // 你的源数据 signal
    
        complexSummary = computed(() => {
    
          // ... 在这里执行你的复杂计算
    
          return this.item().value * 100 + '%';
    
        });
    
    }
    
    <!-- component.html -->
    
    <div>{{ complexSummary() }}</div>

    computed 的「记忆化」特性,保证了只有当其依赖的源 signalitem)变化时,计算逻辑才会重新执行。这是最优雅、最高效的解法。

  2. 备选(经典解法):使用纯管道 (Pure Pipe)。

    <div>{{ item | getComplexSummary }}</div>

    默认情况下,管道是「纯」的。Angular 只在管道的输入值item 的引用,而非内容)发生变化时,才会重新执行管道的 transform 方法。「哲学家」现在只在拿到一本新书时,才会重新思考。

结语#

性能优化,从不是一蹴而就的「灵丹妙药」,它是一系列良好习惯的集合。避开这些「天坑」,本质上是要求我们拥抱 Angular 的演进方向:更细粒度的响应式(Signals)、更明确的模板语法(@fortrack)、以及更清晰的职责划分(计算与渲染分离)。

现在,去检查一下你的代码吧,看看你的「爱车」是不是也掉进了这些坑里。把它拉出来,清洗干净,然后,尽情享受全速飞驰的快感吧!

《礼记·中庸》有云:

知其然,知其所以然。

不仅要懂得事物表象,更要明白其内在的道理和原因。

性能优化亦是如此,我们不应止步于「怎么做」,更要追问「为什么要这么做」,理解框架设计的精髓,才能真正做到游刃有余,让我们的应用如虎添翼,而非疲于奔命。