diff --git a/packages/core/src/render3/instructions/defer.ts b/packages/core/src/render3/instructions/defer.ts index 91bbd25306c6b5..e7e389d844210f 100644 --- a/packages/core/src/render3/instructions/defer.ts +++ b/packages/core/src/render3/instructions/defer.ts @@ -42,15 +42,6 @@ function shouldTriggerDeferBlock(injector: Injector): boolean { return isPlatformBrowser(injector); } -/** - * Shims for the `requestIdleCallback` and `cancelIdleCallback` functions for environments - * where those functions are not available (e.g. Node.js). - */ -const _requestIdleCallback = - typeof requestIdleCallback !== 'undefined' ? requestIdleCallback : setTimeout; -const _cancelIdleCallback = - typeof requestIdleCallback !== 'undefined' ? cancelIdleCallback : clearTimeout; - /** * Creates runtime data structures for `{#defer}` blocks. * @@ -166,11 +157,7 @@ export function ɵɵdeferOnIdle() { const tNode = getCurrentTNode()!; renderPlaceholder(lView, tNode); - - // Note: we pass an `lView` as a second argument to cancel an `idle` - // callback in case an LView got destroyed before an `idle` callback - // is invoked. - onIdle(() => triggerDeferBlock(lView, tNode), lView); + onIdle(() => triggerDeferBlock(lView, tNode), lView, true /* withLViewCleanup */); } /** @@ -195,7 +182,8 @@ export function ɵɵdeferPrefetchOnIdle() { // an underlying LView get destroyed (thus passing `null` as a second argument), // because there might be other LViews (that represent embedded views) that // depend on resource loading. - const cleanupFn = onIdle(() => triggerPrefetching(tDetails, lView), null /* LView */); + const prefetch = () => triggerPrefetching(tDetails, lView); + const cleanupFn = onIdle(prefetch, lView, false /* withLViewCleanup */); registerTDetailsCleanup(injector, tDetails, key, cleanupFn); } } @@ -473,32 +461,28 @@ function registerDomTrigger( * Helper function to schedule a callback to be invoked when a browser becomes idle. * * @param callback A function to be invoked when a browser becomes idle. - * @param lView An optional LView that hosts an instance of a defer block. LView is - * used to register a cleanup callback in case that LView got destroyed before - * callback was invoked. In this case, an `idle` callback is never invoked. This is - * helpful for cases when a defer block has scheduled rendering, but an underlying - * LView got destroyed prior to th block rendering. - */ -function onIdle(callback: VoidFunction, lView: LView|null): VoidFunction { - let id: number; - const removeIdleCallback = () => _cancelIdleCallback(id); - id = _requestIdleCallback(() => { - removeIdleCallback(); - if (lView !== null) { - // The idle callback is invoked, we no longer need - // to retain a cleanup callback in an LView. - removeLViewOnDestroy(lView, removeIdleCallback); - } - callback(); - }) as number; - - if (lView !== null) { - // Store a cleanup function on LView, so that we cancel idle - // callback in case this LView is destroyed before a callback - // is invoked. - storeLViewOnDestroy(lView, removeIdleCallback); - } - return removeIdleCallback; + * @param lView LView that hosts an instance of a defer block. + * @param withLViewCleanup A flag that indicates whether a scheduled callback + * should be cancelled in case an LView is destroyed before a callback + * was invoked. + */ +function onIdle(callback: VoidFunction, lView: LView, withLViewCleanup: boolean) { + const injector = lView[INJECTOR]!; + const scheduler = injector.get(OnIdleScheduler); + const cleanupFn = () => scheduler.remove(callback); + const _callback = withLViewCleanup ? wrapWithLViewCleanup(callback, lView, cleanupFn) : callback; + scheduler.add(_callback); + return cleanupFn; +} + +function wrapWithLViewCleanup( + callback: VoidFunction, lView: LView, cleanup: VoidFunction): VoidFunction { + const _callback = () => { + callback(); + removeLViewOnDestroy(lView, cleanup); + }; + storeLViewOnDestroy(lView, cleanup); + return _callback; } /** @@ -971,3 +955,77 @@ class DeferBlockCleanupManager { factory: () => new DeferBlockCleanupManager(), }); } + +/** + * Shims for the `requestIdleCallback` and `cancelIdleCallback` functions for environments + * where those functions are not available (e.g. Node.js). + */ +const _requestIdleCallback = + typeof requestIdleCallback !== 'undefined' ? requestIdleCallback : setTimeout; +const _cancelIdleCallback = + typeof requestIdleCallback !== 'undefined' ? cancelIdleCallback : clearTimeout; + +/** + * Helper service to schedule `requestIdleCallback`s for batches of defer blocks, + * to avoid scheduling `requestIdleCallback` for each defer block (e.g. if + * a number of defer blocks are created inside a for loop). + */ +class OnIdleScheduler { + executingCallbacks = false; + idleId: number|null = null; + current = new Set(); + deferred = new Set(); + + add(callback: VoidFunction) { + const target = this.executingCallbacks ? this.deferred : this.current; + target.add(callback); + if (this.idleId === null) { + this.scheduleIdleCallback(); + } + } + + remove(callback: VoidFunction) { + this.current.delete(callback); + this.deferred.delete(callback); + } + + private scheduleIdleCallback() { + const callback = () => { + _cancelIdleCallback(this.idleId!); + this.idleId = null; + this.executingCallbacks = true; + + for (const callback of this.current) { + callback(); + } + this.current.clear(); + + this.executingCallbacks = false; + + if (this.deferred.size > 0) { + for (const callback of this.deferred) { + this.current.add(callback); + } + this.deferred.clear(); + this.scheduleIdleCallback(); + } + }; + this.idleId = _requestIdleCallback(callback) as number; + } + + ngOnDestroy() { + if (this.idleId !== null) { + _cancelIdleCallback(this.idleId); + this.idleId = null; + } + this.current.clear(); + this.deferred.clear(); + } + + /** @nocollapse */ + static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({ + token: OnIdleScheduler, + providedIn: 'root', + factory: () => new OnIdleScheduler(), + }); +}