Skip to content

Instantly share code, notes, and snippets.

@e-oz
Last active March 16, 2024 12:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save e-oz/4d64dd47699d3a63d15572ca49dc3db3 to your computer and use it in GitHub Desktop.
Save e-oz/4d64dd47699d3a63d15572ca49dc3db3 to your computer and use it in GitHub Desktop.
import { EventEmitter, NgZone } from "@angular/core";
import { Observable, throttle } from "rxjs";
export class NoopNgZone implements NgZone {
readonly hasPendingMicrotasks = false;
readonly hasPendingMacrotasks = false;
readonly isStable = true;
readonly onUnstable = new EventEmitter<any>();
readonly onError = new EventEmitter<any>();
private readonly onStableEmitter = new EventEmitter<any>();
private readonly onMicrotaskEmptyEmitter = new EventEmitter<any>();
private readonly schedule = getCallbackScheduler();
get onMicrotaskEmpty() {
this.schedule(() => this.onMicrotaskEmptyEmitter.emit({}));
return this.onMicrotaskEmptyEmitter.pipe(
// coalescing - remove this line if it brings issues
throttle(() => getObservableScheduler(this.schedule), { leading: true, trailing: false }),
) as EventEmitter<any>;
}
get onStable() {
this.schedule(() => this.onStableEmitter.emit({}));
return this.onStableEmitter.pipe(
// coalescing - remove this line if it brings issues
throttle(() => getObservableScheduler(this.schedule), { leading: true, trailing: false }),
) as EventEmitter<any>;
}
run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any): T {
return fn.apply(applyThis, applyArgs);
}
runGuarded<T>(
fn: (...args: any[]) => any,
applyThis?: any,
applyArgs?: any
): T {
return fn.apply(applyThis, applyArgs);
}
runOutsideAngular<T>(fn: (...args: any[]) => T): T {
return fn();
}
runTask<T>(
fn: (...args: any[]) => T,
applyThis?: any,
applyArgs?: any,
_name?: string
): T {
return fn.apply(applyThis, applyArgs);
}
emitOnStable() {
this.onStableEmitter.emit({});
}
emitOnMicrotaskEmpty() {
this.onMicrotaskEmptyEmitter.emit({});
}
}
export function provideNoopNgZone() {
return { provide: NgZone, useClass: NoopNgZone };
}
export function getObservableScheduler(scheduler: Function): Observable<boolean> {
return new Observable<boolean>((subscriber) => {
scheduler(() => subscriber.next(true));
});
}
const global: any = globalThis;
/**
* In this file, code below is copied from:
* https://github.com/angular/angular/blob/5328be6660b7ef388720edab4b3f2d0fa2699203/packages/core/src/util/callback_scheduler.ts
*
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*
* Gets a scheduling function that runs the callback after the first of setTimeout and
* requestAnimationFrame resolves.
*
* - `requestAnimationFrame` ensures that change detection runs ahead of a browser repaint.
* This ensures that the create and update passes of a change detection always happen
* in the same frame.
* - When the browser is resource-starved, `rAF` can execute _before_ a `setTimeout` because
* rendering is a very high priority process. This means that `setTimeout` cannot guarantee
* same-frame create and update pass, when `setTimeout` is used to schedule the update phase.
* - While `rAF` gives us the desirable same-frame updates, it has two limitations that
* prevent it from being used alone. First, it does not run in background tabs, which would
* prevent Angular from initializing an application when opened in a new tab (for example).
* Second, repeated calls to requestAnimationFrame will execute at the refresh rate of the
* hardware (~16ms for a 60Hz display). This would cause significant slowdown of tests that
* are written with several updates and asserts in the form of "update; await stable; assert;".
* - Both `setTimeout` and `rAF` are able to "coalesce" several events from a single user
* interaction into a single change detection. Importantly, this reduces view tree traversals when
* compared to an alternative timing mechanism like `queueMicrotask`, where change detection would
* then be interleaves between each event.
*
* By running change detection after the first of `setTimeout` and `rAF` to execute, we get the
* best of both worlds.
*/
export function getCallbackScheduler(): (callback: Function) => void {
// Note: the `getNativeRequestAnimationFrame` is used in the `NgZone` class, but we cannot use the
// `inject` function. The `NgZone` instance may be created manually, and thus the injection
// context will be unavailable. This might be enough to check whether `requestAnimationFrame` is
// available because otherwise, we'll fall back to `setTimeout`.
const hasRequestAnimationFrame = typeof global['requestAnimationFrame'] === 'function';
let nativeRequestAnimationFrame =
hasRequestAnimationFrame ? global['requestAnimationFrame'] : null;
let nativeSetTimeout = global['setTimeout'];
// @ts-ignore
if (typeof Zone !== 'undefined') {
// Note: zone.js sets original implementations on patched APIs behind the
// `__zone_symbol__OriginalDelegate` key (see `attachOriginToPatched`). Given the following
// example: `window.requestAnimationFrame.__zone_symbol__OriginalDelegate`; this would return an
// unpatched implementation of the `requestAnimationFrame`, which isn't intercepted by the
// Angular zone. We use the unpatched implementation to avoid another change detection when
// coalescing tasks.
// @ts-ignore
const ORIGINAL_DELEGATE_SYMBOL = (Zone as any).__symbol__('OriginalDelegate');
if (nativeRequestAnimationFrame) {
nativeRequestAnimationFrame =
(nativeRequestAnimationFrame as any)[ORIGINAL_DELEGATE_SYMBOL] ??
nativeRequestAnimationFrame;
}
nativeSetTimeout = (nativeSetTimeout as any)[ORIGINAL_DELEGATE_SYMBOL] ?? nativeSetTimeout;
}
return (callback: Function) => {
let executeCallback = true;
nativeSetTimeout(() => {
if (!executeCallback) {
return;
}
executeCallback = false;
callback();
});
nativeRequestAnimationFrame?.(() => {
if (!executeCallback) {
return;
}
executeCallback = false;
callback();
});
};
}
@e-oz
Copy link
Author

e-oz commented Mar 4, 2024

Usage example:

bootstrapApplication(AppComponent, {
  providers: [
    ɵprovideZonelessChangeDetection(), // imported from `@angular/core`
    provideNoopNgZone(), // imported from this helper
  ]
}).catch(err => console.error(err));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment