Angular v17のDeferrable Viewsと内部実装の話
Deferrable Viewsの話
Angular v17の新機能
Deferrable views • Angular
@defer ブロックで囲われたビューに含まれるコンポーネントは自動的にチャンク分割され、遅延読み込みされる。
初期バンドルサイズの削減により読み込み時間の短縮に効果あり
code:html
@defer {
<large-component />
}
遅延読み込みを開始するためのいろいろなトリガーがある
code:html
@defer (on TRIGGER) {
<large-component />
}
各トリガーはどのように実装されているのか?
on interaction
click と keydown イベントをリッスンしている。
つまりマウスとキーボードの両対応
https://github.com/angular/angular/blob/main/packages/core/src/defer/dom_triggers.ts#L67
code:ts
/** Names of the events considered as interaction events. */
const interactionEventNames = 'click', 'keydown' as const;
// ...
for (const name of interactionEventNames) {
trigger.addEventListener(name, entry!.listener, eventListenerOptions);
}
on hover
mouseenter と focusin イベントをリッスンしている。
Element: focusin イベント - Web API | MDN
つまりマウスとキーボードの両対応
https://github.com/angular/angular/blob/main/packages/core/src/defer/dom_triggers.ts#L115
code:ts
/** Names of the events considered as hover events. */
const hoverEventNames = 'mouseenter', 'focusin' as const;
for (const name of hoverEventNames) {
trigger.addEventListener(name, entry!.listener, eventListenerOptions);
}
on viewport
少し複雑だが、結局 IntersectionObserver を使っている。
isIntersecting した要素が viewport トリガーのターゲット要素だったら発火する。
https://github.com/angular/angular/blob/main/packages/core/src/defer/dom_triggers.ts#L152
code:ts
intersectionObserver = intersectionObserver || ngZone.runOutsideAngular(() => {
return new IntersectionObserver(entries => {
for (const current of entries) {
// Only invoke the callbacks if the specific element is intersecting.
if (current.isIntersecting && viewportTriggers.has(current.target)) {
ngZone.run(viewportTriggers.get(current.target)!.listener);
}
}
});
});
if (!entry) {
entry = new DeferEventEntry();
ngZone.runOutsideAngular(() => intersectionObserver!.observe(trigger));
viewportTriggers.set(trigger, entry);
observedViewportElements++;
}
on idle
詳細は割愛するが、ブラウザが requestIdleCallback をサポートしていれば使い、サポートされてなければ setTimeout にフォールバックしている。
requestIdleCallback | Can I use... Support tables for HTML5, CSS3, etc
相変わらずSafari系では実装されていないため、idle トリガーの振る舞いはSafariとそれ以外で変わる可能性がある。
https://github.com/angular/angular/blob/main/packages/core/src/defer/idle_scheduler.ts#L19
code:ts
const _requestIdleCallback = () =>
typeof requestIdleCallback !== 'undefined' ? requestIdleCallback : setTimeout;
const _cancelIdleCallback = () =>
typeof requestIdleCallback !== 'undefined' ? cancelIdleCallback : clearTimeout;
export function onIdle(callback: VoidFunction, lView: LView) {
const injector = lViewINJECTOR!;
const scheduler = injector.get(IdleScheduler);
const cleanupFn = () => scheduler.remove(callback);
scheduler.add(callback);
return cleanupFn;
}
on timer
結構複雑にタイマーが実装されている
https://github.com/angular/angular/blob/main/packages/core/src/defer/timer_scheduler.ts#L17
ここがおもしろい https://github.com/angular/angular/blob/main/packages/core/src/defer/timer_scheduler.ts#L117
負荷がかかりすぎないように、60fpsよりも短いスパンにならないよう調整されている。
つまり、timer トリガーで16msより短い指定はできない。
まとめ
@defer はいいぞ
いまどきのフレームワークの機能はデフォルトでアクセシビリティが考慮されている。
requestIdleCallback を早く普通に使いたい。