哈囉,各位邦友們!
昨日把專案部署到 GitHub Pages。
接下來就該面對效能,視角放回 Angular 的變更檢測,並探討新版框架提供的 zoneless 模式,看看 Signals 如何帶我們走向更俐落的更新策略。
回顧 Day02,我們啟動專案時就勾選了「zoneless」的選項;app.config.ts
也透過 provideZonelessChangeDetection()
宣示這項設定。要理解它的價值,得先來回顧 Zone.js。
setTimeout
、事件、Promise 這類非同步操作,結束時自動呼叫 ApplicationRef.tick()
,整棵元件樹都被迫重新檢查。change detection
,成本就開始累積。NgZone.runOutsideAngular()
或手動 ChangeDetectorRef.markForCheck()
來抑制波動。想像我們在英雄資料介面加了一個「戰鬥冷卻倒數」的功能,過去常見的寫法大概長這樣:
import { Component } from '@angular/core';
@Component({
selector: 'app-cooldown-timer',
imports: [],
templateUrl: './cooldown-timer.html',
styleUrl: './cooldown-timer.scss',
})
export class CooldownTimer {
seconds = 10;
ngOnInit() {
setInterval(() => {
this.seconds--;
}, 1000);
}
}
在 Zone.js 底下,即使只有 seconds
這個欄位被更新,每秒仍會觸發一次整體變更檢測。隨著功能增加、元件樹變深,這種「地圖砲式」更新會造成:
@for
清單或 @defer
區塊。NgZone
、ChangeDetectorRef
,在 runOutsideAngular()
與 run()
之間反覆切換,程式碼越寫越複雜。這就是 outline 說的 Zone.js 痛點:就算只是 setTimeout
或 setInterval
,也會對整個應用造成不成比例的刷新成本。
Angular v17 之後提供的 provideZonelessChangeDetection()
目標非常直接:拿掉 Zone.js 的全域攔截,改由響應式狀態決定哪些畫面需要更新。Signals 就是這個年代的主角。
在我們的專案中,HeroService
早於 Day21 就把資料放進 signal
,組件則透過 computed
、effect
、resource()
將狀態傳遞到 UI。搭配 zoneless 模式時,運作流程變得更乾淨:
this.cooldown.update(v => v - 1)
)。effect
,而不是遍歷整棵 component tree。重點是:框架從「外部攔截一切事件再回來檢查」進化為「內部精準追蹤資料流」。
讓我們把前述範例改寫成符合 Day21~Day23 架構的版本:
ng g c cooldown-timer
// src/app/cooldown-timer/cooldown-timer.ts
import { Component, DestroyRef, inject, signal } from '@angular/core';
@Component({
selector: 'app-cooldown-timer',
imports: [],
templateUrl: './cooldown-timer.html',
styleUrl: './cooldown-timer.scss',
})
export class CooldownTimer {
private readonly destroyRef = inject(DestroyRef);
readonly seconds = signal(10);
constructor() {
const timerId = window.setInterval(() => {
this.seconds.update((value) => Math.max(0, value - 1));
}, 1000);
this.destroyRef.onDestroy(() => window.clearInterval(timerId));
}
}
<!-- src/app/cooldown-timer/cooldown-timer.html -->
剩餘 {{ seconds() }} 秒
setInterval
仍然運作,但不再依賴 Zone.js;每次執行時直接更新 signal
即可。{{ seconds() }}
只會在 seconds
變動時重新繪製,其他畫面完全不受影響。DestroyRef.onDestroy()
清除計時器,維持 Day19~Day23 強調的資源管理習慣。將這段元件掛進 Heroes 頁面後,你可以使用 Chrome Performance 或 Angular DevTools 查看,每一秒只會更新這個計時器對應的 DOM,而非整個應用。
經過 Day01~Day27 的累積,我們其實已具備進入 zoneless 的條件。導入時可以依照以下清單檢視:
signal
、computed
、resource()
分享。Day21~Day23 的重構是關鍵前置作業。setTimeout
與共享變數,改以 RxJS 或 requestAnimationFrame
轉成 signal,使更新來源一致。provideBrowserGlobalErrorListeners()
(同樣寫在 app.config.ts
)會在 zoneless 模式下協助捕捉異常,記得在 effect 中適度處理例外,避免失敗狀態被吞掉。NgZone
的舊程式碼(例如 Day08 以前的版本),可以建立暫時的 ZonelessCompatService
,在需要時手動觸發 markDirty()
,逐步汰除舊寫法。Change detection cycles
是否下降,讓優化變得有憑有據。今日小結:
Zone.js 曾陪伴我們度過 Angular 初期的學習曲線,但當應用規模擴大,它的「全域攔截」模式也帶來了不必要的成本。
透過今天的練習,你應該感受到 zoneless + Signals 的組合能夠更精準地更新畫面,並提升部署後的效能品質。