Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(core): avoid unnecessary callbacks in after render hooks #52292

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
47 changes: 26 additions & 21 deletions packages/core/src/render3/after_render_hooks.ts
Expand Up @@ -130,6 +130,11 @@ export interface InternalAfterNextRenderOptions {
injector?: Injector;
}

/** `AfterRenderRef` that does nothing. */
const NOOP_AFTER_RENDER_REF: AfterRenderRef = {
destroy() {}
};

/**
* Register a callback to run once before any userspace `afterRender` or
* `afterNextRender` callbacks.
Expand Down Expand Up @@ -211,24 +216,21 @@ export function afterRender(callback: VoidFunction, options?: AfterRenderOptions
const injector = options?.injector ?? inject(Injector);

if (!isPlatformBrowser(injector)) {
return {destroy() {}};
return NOOP_AFTER_RENDER_REF;
}

let destroy: VoidFunction|undefined;
const unregisterFn = injector.get(DestroyRef).onDestroy(() => destroy?.());
const afterRenderEventManager = injector.get(AfterRenderEventManager);
// Lazily initialize the handler implementation, if necessary. This is so that it can be
// tree-shaken if `afterRender` and `afterNextRender` aren't used.
const callbackHandler = afterRenderEventManager.handler ??= new AfterRenderCallbackHandlerImpl();
const ngZone = injector.get(NgZone);
const errorHandler = injector.get(ErrorHandler, null, {optional: true});
const phase = options?.phase ?? AfterRenderPhase.MixedReadWrite;
const instance = new AfterRenderCallback(ngZone, errorHandler, phase, callback);

destroy = () => {
const destroy = () => {
callbackHandler.unregister(instance);
unregisterFn();
};
const unregisterFn = injector.get(DestroyRef).onDestroy(destroy);
const instance = new AfterRenderCallback(injector, phase, callback);

callbackHandler.register(instance);
return {destroy};
}
Expand Down Expand Up @@ -288,27 +290,24 @@ export function afterNextRender(
const injector = options?.injector ?? inject(Injector);

if (!isPlatformBrowser(injector)) {
return {destroy() {}};
return NOOP_AFTER_RENDER_REF;
}

let destroy: VoidFunction|undefined;
const unregisterFn = injector.get(DestroyRef).onDestroy(() => destroy?.());
const afterRenderEventManager = injector.get(AfterRenderEventManager);
// Lazily initialize the handler implementation, if necessary. This is so that it can be
// tree-shaken if `afterRender` and `afterNextRender` aren't used.
const callbackHandler = afterRenderEventManager.handler ??= new AfterRenderCallbackHandlerImpl();
const ngZone = injector.get(NgZone);
const errorHandler = injector.get(ErrorHandler, null, {optional: true});
const phase = options?.phase ?? AfterRenderPhase.MixedReadWrite;
const instance = new AfterRenderCallback(ngZone, errorHandler, phase, () => {
destroy?.();
callback();
});

destroy = () => {
const destroy = () => {
callbackHandler.unregister(instance);
unregisterFn();
};
const unregisterFn = injector.get(DestroyRef).onDestroy(destroy);
const instance = new AfterRenderCallback(injector, phase, () => {
destroy();
callback();
});

callbackHandler.register(instance);
return {destroy};
}
Expand All @@ -317,9 +316,15 @@ export function afterNextRender(
* A wrapper around a function to be used as an after render callback.
*/
class AfterRenderCallback {
private zone: NgZone;
private errorHandler: ErrorHandler|null;

constructor(
private zone: NgZone, private errorHandler: ErrorHandler|null,
public readonly phase: AfterRenderPhase, private callbackFn: VoidFunction) {}
injector: Injector, public readonly phase: AfterRenderPhase,
private callbackFn: VoidFunction) {
this.zone = injector.get(NgZone);
this.errorHandler = injector.get(ErrorHandler, null, {optional: true});
}

invoke() {
try {
Expand Down