Skip to content

Instantly share code, notes, and snippets.

@fredjoseph
Created April 22, 2022 15:05
Show Gist options
  • Save fredjoseph/4f0292d0954f4c2b8a700d7e45089eaa to your computer and use it in GitHub Desktop.
Save fredjoseph/4f0292d0954f4c2b8a700d7e45089eaa to your computer and use it in GitHub Desktop.
Angular Performance debugging

This gist is a small summary of debug-angular-performance-better-with-these-tools

Mornitoring Angular's Change Detection Time

In my AppModule, I first inject ApplicationRef in the constructor and patch the tick() function with some logging code:

export class AppModule {
    constructor(applicationRef: ApplicationRef) {

        if (isDevMode()) {
            // First, store the original tick function
            const originalTick = applicationRef.tick

            applicationRef.tick = function () {
            	// Save start time
                const windowsPerfomance = window.performance
                const before = windowsPerfomance.now()
                
                // Run the original tick() function
                const returnValue = originalTick.apply(this, arguments)
                
                // Save end time, calculate the delta, then log to console
                const after = windowsPerfomance.now()
                const runTime = after - before
                window.console.log('[Profiler] CHANGE DETECTION TIME', runTime, 'ms')
                return returnValue

            }
        }
    }
}

Decorator

See profiler.ts Usage:

@ProfileClassToConsole()
@Component({
	// ...
})
export class MyComponent {
	// ...
}

Angular Debug Tools

To enable these tools, you need to add a few line of code in your src/main.ts.

import { ApplicationRef, isDevMode } from '@angular/core'
import { enableDebugTools } from '@angular/platform-browser'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'

import { AppModule } from './app/app.module'

// ... other bootstrap code

platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .then(moduleRef => {

        if (isDevMode()) {
            // Enable console debug tools
            const appRef = moduleRef.injector.get(ApplicationRef)
            const componentRef = appRef.components[0]

            enableDebugTools(componentRef)
        }
    })
    .catch(err => console.error(err))

ng.profiler.timeChangeDetection()

One of the tool enabled is ng.profiler.timeChangeDetection(). This funtion basically run the app's root CD a few time and print out average time required.

In your browser's Dev Console, type ng.profiler.timeChangeDetection()

ng.probe()/ng.getComponent()

Angular dev tool provide us with a function that allow us to tap into a component's state and infomation at runtime, as well as altering some of its state (or call its internal methods). We were on Angular 5 when I worked on these so ng.probe().componentInstance is what I used. If you are on version 9 and above with Ivy compiler, the function has changed to ng.getComponent().

In Chrome, if you select something in the inspector, you may notice it says == $0 at the end. If you use the inspector to select any Angular Component (e.g. tag) and pass it to ng.probe() (ng.probe($0)), it will return something interesting

Note, with ng.probe(), you can actually select any of the component's child node, as long as that node is not an Angular Component itself, and ng.probe() will find the right Angular Component. With ng.getComponent() however, you will need to select the Angular Component's selector tag itself.

Injecting ChangeDetectorRef

To get the most out of ng.probe() for performance debugging, you can inject ChangeDetectorRef into your component. This will allow us to access the component's change detector at runtime.

You can use detach() and reattach() to manually turn CD off and on for a particular component, enabling you to somewhat isolate the effect of a component's change detection cycle on overall app's performance. For example, if detaching a component's CD reduce the overall CD time by 50ms then you can say that the component or some of its children is costing around 50ms of time to complete its CD.

import { isDevMode } from '@angular/core'
/**
* Use to patch all functions/methods in a class and make them print out run time
* in ms to the console.
*
* This decorator will only patch functions declared in the target class.
* It will **not** patch functions reside in the **base class**.
* Dynamically created functions or functions received from the outside as input
* may also not be patched.
*
* Keep in mind that the act of printing stuffs to the console itself will slow down
* some function a little. This could add up if that function is called multiple times in a loop.
* Callbacks may also not be tracked so functions that rely on
* callbacks to do heavy lifting may appear to take very little time
* here.
*
* Usage:
* @ProfileClassToConsole()
* export class ClassToProfile {
* ....
* }
*
* @param threshold allow filtering log to only those that took more than the threshold (in ms)
*/
export function ProfileClassToConsole({ prefix = '', threshold = 0 } = {}) {
return function (target: Function) {
// Guard to skip patching
if (!isDevMode()) {
return
}
// Loop through all property of the class
for (const propName of Object.keys(target.prototype)) {
const descriptor = Object.getOwnPropertyDescriptor(target.prototype, propName)
// If not a function, skip
if (!(descriptor.value instanceof Function)) {
continue
}
const windowsPerfomance = window.performance
const fn = descriptor.value
descriptor.value = function (...args: any[]): any {
const before = windowsPerfomance.now()
const result = fn.apply(this, args)
const after = windowsPerfomance.now()
const runTime = after - before
if (runTime > threshold) {
console.log(prefix, target.name, ': ', propName, 'took', runTime, 'ms')
}
return result
}
Object.defineProperty(target.prototype, propName, descriptor)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment