Angular 17 推出 Deferrable views 的功能,使在樣版裡可以指定 條件 和 依賴 延遲的區塊,包括了載入(components, directives, pipes, 和有關連的 CSS),把所有需要 延遲的區塊放置 @defer { ... } 中。
可延遲視圖有支援許多條件的觸發器(on idle、on viewport、on interaction、on hover、on immediate、on timer) 和 子區塊(@placeholder、@loading、@error),也可以使用 when 和 prefetch when 自定條件。
看完本文章之後,就可以明白要如何使用 @defer 的語法,以及 內建(predefined) 和 自定(custom) 的 觸發器(triggers),和比較 @defer 和 lazy loading的不同。
[ TypeScript ]
@defer {
<large-component />
}
注意:很重要! 很重要! 很重要!
@defer 只是作 區塊延遲載入,沒有辦法取代 @if、等等。
那為什麼我們需要用新的 @defer 來進行可延遲檢視圖(Deferrable Views),因為 Angular 本來就有了路由式的延遲載入(router-based lazy loading), 可是我們還是要更快把網頁載入速度減少的方法,當使用者來參觀你的網頁,3秒沒有展開就會關掉網頁。
所以當你的頁面上的區塊越多,載入完成就會越慢,所以我們要想儘辦法加速載入的速度,終於在 Angular 17 支援頁面區塊 - 可延遲檢視圖(Deferrable Views),不過還是要小心延遲載入造成網頁版本變形破版。
@defer 能夠做到什麼呢?
- 當使用者 "瀏覽" 到某一個位置時,載入較大的區塊。
- 當使用者 "點擊" 某一個位置時,載入較大的區塊。
- 當使用者在使用時,在背景中 "預先" 載入較大的區塊。
為了讓 @defer 區塊內的依賴能夠被延遲加載,它們需要滿足兩個條件:
- 它們必須是獨立的(standalone)。非獨立的依賴無法被延遲加載,即使它們在 @defer 區塊內部也是如此。
- 它們不能直接從同一個文件外部引用,包括 ViewChild 查詢。
在下面的例子,我們建立了一個 sub-block 的元件(component),使用 @defer 將 sub-block 延遲載入。
[ TypeScript ]
import { Component } from '@angular/core';
import {} from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { SubBlockComponent } from './sub-block.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, SubBlockComponent],
template: `
<div>
<div><router-outlet></router-outlet></div>
<div>
@defer {
<app-sub-block></app-sub-block>
}
</div>
</div>
`,
styles: [],
})
export class AppComponent {
title = 'app';
}
讓我們執行 ng serve 來看看下圖的 紅色框框 的 sub-block 的元件,現在就是在 Lazy chuck files 的區塊了。
讓我們打開網頁來試試結果,下圖 sub-block元件就在最後才載入。
@defer 提供了我們許多的功能( @placeloader、@loading、@error ),讓我們在許多功能需求都可以達成。
@defer with @placeloader
@placeloader 是與 @defer 一起使用的,提供我們在延遲載入時的預設顯示的畫面,當進行載入時就會消失的區塊,我們可以拿來做簡單的區塊或是文字,來呈現在畫面上。
注意:官方說不建議在 @placeloader 區塊裡,放入複雜的元件(components)、組件(directives)、管道(pipes)、…等等。
[ TypeScript ]
@defer {
<app-sub-block></app-sub-block>
}
@placeholder {
<span>This is a sub-block component.</span>
}
在上面的例子,@placeholder 裡的 "This is a sub-block component.",會在一開始就出現,不過只會出現一瞬間出現,就會消失了,這樣會造成使用者覺是不是發生什麼錯誤了。
@placeholder parameters
所以在 @placeholder 有提供了一個參數為 "minimum",時間週期以秒(s)和毫秒(ms)為單位。
下面的例子 @placeholder 的 minimum 接受2種方式
- 數值 + s ==> 2s (秒)
- 數值 ==> 2000 (毫秒)
[ TypeScript ]
# in seconds
@defer {
<app-sub-block></app-sub-block>
} @placeholder ( minimum 2s ) {
<span>This is a sub-block component.</span>
}
# in milliseconds
@defer {
<app-sub-block></app-sub-block>
} @placeholder ( minimum 2000 ) {
<span>This is a sub-block component.</span>
}
@defer with @loading
@defer 另一個搭配使用 @loading,觸發時機為,在載入 "延遲載入" 元件,就是在下載單獨的 js 時會顯示在畫面上。
[ TypeScript ]
@defer {
<app-sub-block></app-sub-block>
} @loading {
<span>Loading...</span>
}
注意:官方說不建議在 @loading 區塊裡,放入複雜的元件(components)、組件(directives)、管道(pipes)、…等等。
上面的例子裡,當在下載 <app-sub-block></app-sub-block> 時,才會顯示 <span>Loading...</span>。
不過在測式環境不增加任何的參數,難以顯示畫面,連一閃都看不到,因為下載的速度太快了。
如果還是要測試出來的話,那就要調整 Google Dveloper Tools-> Network -> 模擬 Slow 3G 的速度才會顯示出 <span>Loading...</span> 在畫面上。
@loading parameters
@loading 提供了2個可選參數
- minimum 設定顯示區塊最短的時間。
- after 當觸發下載時,等待多少時間才顯示區塊
注意:after 參數所設定時間,必需 大於 下載的 真實時間,如果 小於 的話 minimum 參數就會失效。
[ TypeScript ]
# minimum 5s
@defer {
<app-sub-block></app-sub-block>
} @loading (minimum 5s) {
<span>Loading...</span>
}
# after 2s; minimum 5s
@defer {
<app-sub-block></app-sub-block>
} @loading (after 2s; minimum 5s) {
<span>Loading...</span>
}
上面的是單獨設定 minimum 和 after & minimum的例子,設定的方式都是一樣的 秒 和 毫秒。
不過上面 設定 after 為 2秒,下載這個元件的真實時間為 1秒,這樣的話 minimum 不管設定什麼都會失效。
@placeholder & @loading 有什麼不同,以目前看來功能一差不多,就是不同階段顯示不同的畫面區塊,不過個人是以下簡單的流程就會比較清楚的不同。
@placeholder(非必要)(條件式) => @defer(必要)(條件式) => @loading(非必要)(條件式)
@defer with @error
@error 這個區塊就是當發生下載錯誤時 或 無論是什麼原因造成的錯誤,就會觸發 @error 區塊。
[ TypeScript ]
@defer {
<app-sub-block></app-sub-block>
} @error {
<span>Error...</span>
}
How do @defer triggers work?
接下來終於進入如何觸發 @defer 的選項的功能,可分為下面二大部份:
- (可選的) 何時預先(prefetch)載入。
- (可選的) 何時顯示區塊的觸發器。
這二大部份是非常不同的事件,可以分別控制並靈活使用,系統有提供預設的觸發器(使用關鍵字 on),也可以自定觸發器(使用關鍵字 when)。
系統預設的觸發器有下列(使用關鍵字 on):
- idle (閒置)
- viewport (區塊顯示)
- interaction (互動)
- hover (移進移出)
- immediate (主即)
- timer (計時器)
我們會一一來學習這些系統預定觸發器
預設觸發器 idle ( The idle built-in trigger )
注意:以下二種 @defer 區塊是相同的功能,可以看到是使用 on 的關鍵字,下面使用 idle 的例子,同時是 預設 和 預先(prefetch) 的觸發器。
[ TypeScript ]
@defer {
<app-sub-block></app-sub-block>
}
@defer (on idle; prefetch on idle) {
<app-sub-block></app-sub-block>
}
上面的例子,Angular 使用瀏覽器 api(requestIdleCallback)檢測瀏覽器,完成所有的下載的連線時,通常就會出現 "閒置" 的狀態,所以 @defer 通常都立即觸發。
預設觸發器 viewport 與 @placeholder 相關( The viewport built-in trigger with @placeholder )
viewport 觸發器 可以和 @placeholder 搭配使用,當 @placeholder 區塊顯示在畫面上時,就會觸發 viewport 執行下載區塊的程式。
[ TypeScript ]
@defer (on viewport) {
<app-sub-block></app-sub-block>
} @placeholder {
<span>waiting...</span>
}
viewport 觸發器可以單獨使用,不過要 指定 一個區塊, 當 指定 的區塊 顯示 在畫面上時,viewport 就會觸發下載區塊的程式。
[ TypeScript ]
<div #title>Title</div>
@defer (on viewport(title)) {
<app-sub-block></app-sub-block>
}
預設觸發器 interaction 與 @placeholder 相關( The interaction built-in trigger with @placeholder )
interaction 可以搭配 @placeholder 使用,當 @placeholder 的區塊有 互動 (點擊、輸入、…),interaction 就會觸發下載 @defer 區塊程式。
[ TypeScript ]
@defer (on interaction) {
<app-sub-block></app-sub-block>
} @placeholder {
<div>click</div>
}
interaction 可以單獨使用,同樣的要綁定一個元件,當元件跟使用者 互動 (點擊、輸入…)時,interaction 就會觸發 @defer 下載區塊程式。
[ TypeScript ]
<div #click>click</div>
@defer (on interaction(click)) {
<app-sub-block></app-sub-block>
}
預設觸發器 hover 與 @placeholder 相關( The hover built-in trigger with @placeholder )
hover 可以搭配 @placeholder 使用,當 滑鼠移進 ( 事件 mouseenter、focusin ) @placeholder 的區塊時,hover 就會觸發下載 @defer 區塊程式。
[ TypeScript ]
@defer (on hover) {
<app-sub-block></app-sub-block>
} @placeholder {
<div>hover</div>
}
hover 可以單獨使用,同樣的要綁定一個元件,當 滑鼠移進 ( 事件 mouseenter、focusin ) 元件時,hover 就會觸發 @defer 下載區塊程式。
[ TypeScript ]
<div #action>hover</div>
@defer (on hover(action)) {
<app-sub-block></app-sub-block>
}
預設觸發器 immediate ( The immediate built-in trigger )
immediate 是 立即 觸發 @defer 下載區塊程式,不會被等任何事件,也不會等待 閒置 才載入(idle)。
[ TypeScript ]
@defer (on immediate) {
<app-sub-block></app-sub-block>
}
預設觸發器 timer ( The timer built-in trigger )
timer 就是字面上定義的 計時器,當 計時器 設定的時間結束,timer 就會觸發 @defer 下載區塊程式。
計時器(timer)可以接受 毫秒(ms) 或 秒(s)。
下面2個例子是一樣的,只是使用 秒(s) 或 毫秒(ms) 的參數。
[ TypeScript ]
@defer (on timer(5s)) {
<app-sub-block></app-sub-block>
}
@defer (on timer(5000)) {
<app-sub-block></app-sub-block>
}
注意:如果有使用 --ssr 選項時,timer 也會在 server 端執行 timer 的功能,所以會暫停設定的秒數,整個頁面整個會凍結時間。
預取 @defer 區塊( Prefetching @defer blocks )
上面都是控制何時顯示區塊,接下來我們要學習 預取 ,預取就是預先下載完成,使用 @defer 時,我們有2個層次的控制:
[ TypeScript ]
@defer (on timer(5s)) {
<app-sub-block></app-sub-block>
}
@defer (on timer(5s); prefetch on idle) {
<app-sub-block></app-sub-block>
}
在上面的2個例子是相同的結果,2個例子都是 5秒後 顯示區塊 和 當有 閒置 時就預先下載程式。
下面的例子我們要做不同的 預先下載 的例子:
[ TypeScript ]
@defer (on interaction; prefetch on viewport) {
<app-sub-block></app-sub-block>
} @placeholder {
<input />
}
當 @placeholder 區塊裡的 input 產生 互動 時就顯示 @defer 區塊程式(藍色區塊),當 @placeholder 區塊裡的 input 顯示時,就預先下載 @defer 區塊的程式(紅色區塊)。
上面的例子 預設的觸發器 都可以使用在 預取 和 顯示 @defer 區塊,所以說我們可以 自已組合 所需的運用。
自定 @defer 觸發器(Custom @defer triggers)
Angular 的 @defer 提供可以 自定 觸發器,使用 when 關鍵字來定義相關條件,顯示 和 預先下載區塊 都可分別設定,以下的例子可以看到如何設定:
[ TypeScript ]
@Component({
selector: 'app-root',
standalone: true,
imports: [SubBlockComponent],
template: `
<div>
<button (click)="onLoad()">Trigger Prefetch</button>
<button (click)="onDisplay()">Trigger Display</button>
@defer (when show; prefetch when load) {
<app-sub-block></app-sub-block>
}
</div>
`,
styles: [],
})
export class AppComponent {
load: boolean = false;
show: boolean = false;
onLoad() {
this.load = true;
}
onDisplay() {
this.show = true;
}
}
上面的例子實作了決定 何時(when)預先(prefetch)下載程式、何時(when)顯示區塊,在 藍色區塊(when show) 條件式裡,當 Trigger Prefectch 按鈕被按下去時,就預先(prefetch)下載 <app-sub-block> 程式,在 紅色區塊(prefetch when load) 條件式裡,當 Trigger Display 按鈕被按下去時,就顯示 <app-sub-block> 區塊。
注意:Server-side-rendering 使用 @defer 渲染時會發生什麼事?
- 瀏覽器事件(Browser events)
- 計時器事件(timer event)
- 區塊顯示事件(viewport event)
- 滾動事件(scrolling event)
都不建立使用,因為會 無作用 或 發生錯誤。