Skip to content

Commit

Permalink
refactor(core): add batching for defer blocks with on idle conditions
Browse files Browse the repository at this point in the history
This commit updates runtime logic of defer blocks to schedule a single `requestIdleCallback` for a group of defer blocks created within a single change detection cycle (for example, as a result of a defer block being defined in a for loop).
  • Loading branch information
AndrewKushnir committed Sep 25, 2023
1 parent d6bfebe commit f3051c2
Showing 1 changed file with 99 additions and 41 deletions.
140 changes: 99 additions & 41 deletions packages/core/src/render3/instructions/defer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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 */);
}

/**
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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<VoidFunction>();
deferred = new Set<VoidFunction>();

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(),
});
}

0 comments on commit f3051c2

Please sign in to comment.