Skip to main content Skip to footer

How to Optimize and Create the Fastest Angular Datagrid Application

Optimization is a crucial part of the design process when building internet applications. According to studies, the ideal time for a website to load a page is 2-3 seconds; any longer than that, and the chance that the user will leave the page before it loads increases dramatically, and pages that load faster see increased ad revenue from their userbase.

This is even more crucial when visiting pages on a mobile device, which see an even higher bounce rate than pages loaded on a desktop as the time to load increases. With the increase in users visiting sites on mobile devices, Google has since switched to favoring mobile-first indexing.

This means that optimization is even more crucial, with Google favoring indexing websites based on Googlebot's smartphone agent moving forward.

Today's larger applications deal with large data sets, large bundles and perform many complex calculations. Thankfully, Angular and Wijmo's FlexGrid give you options to optimize your Angular DataGrid application. In this blog, we'll cover the following topics:

  • Unsubscribing From Observables
  • Managing Routing Flow With Resolvers
  • Optimizing Change Detection With ChangeDetectionStrategy.onPush
  • Reducing DOM Elements With FlexGrid Virtualization
  • Lazy Loading Angular Modules

Using FlexGrid with Angular is so easy that it's fun! If you don't believe me, download Wijmo's free trial and follow along.

Download Now!

Unsubscribing from Observables

Observables do exactly what they say; they're things you wish to observe and take action on. They offer an easy way for developers to pass data both to and around their applications.

In Angular, they're used for event handling, asynchronous programming, and managing different sets of data. However, improper use of observables can lead to a decrease in performance and serious memory management issues.

When using observables, they use a subscribe method to get the data and watch to see if there are any changes with the data, which is all done asynchronously.

Typically, you subscribe to a method within a service that makes an HTTP call to get your data, and in most cases, this is done within the ngOnInit() method of your component:

data.service.ts:

export class DataService {
    apiURL = 'https://mocki.io/v1/6f3bb49a-353a-4318-ba77-0474f6fafeed';

    constructor(private http: HttpClient) {}

    getAPIData() {
        return this.http.get(this.apiURL);
    }
}

mock.component.ts:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { DataService } from './data.service';

@Component({...})
export class  MockComponent implements OnInit, OnDestroy {
    mockData: any;
    mockSubscription: any;
    constructor(private dataService: DataService){}

    ngOnInit() {
        this.mockSubscription = this.dataService.getAPIData().subscribe((data) => {
            this.mockData = data;
        });
    }
}

With that complete, we can now asynchronously access the data that we're retrieving from our API and get any changes that happen to the data. However, there's one crucial thing that we're missing: calling the unsubscribe() method on our observable. If we navigate away from this component, that subscription connection remains open, leading to memory leaks, leading to serious performance and security issues.

Since we want this connection to remain open while this component is loaded, we'll wait until the component gets destroyed to unsubscribe. If you noticed in the code sample above, we're also importing OnDestroy, which is a lifecycle hook that gets called whenever the component gets destroyed. All we need to do is implement the ngOnDestroy() method and call unsubscribe() on our observable:

mock.component.ts:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { DataService } from './data.service';

@Component({...})
export class MockComponent implements OnInit, OnDestroy {
    mockData: any;
    mockSubscription: any;
    constructor(private dataService: DataService){}

    ngOnInit() {
        this.mockSubscription = this.dataService.getAPIData().subscribe((data) => {
            this.mockData = data;
        });
    }

    ngOnDestroy() {
        this.mockSubscription.unsubscribe();
    }
}

When our users navigate away from this component, our application will unsubscribe from our observable, preventing any memory leaks. If you'd like to learn more about observables, Angular has documentation that provides more information.

Managing Routing Flow with Resolvers

When we load components that make use of observables to retrieve data through HTTP calls, there will be times where we receive an error from our HTTP request. When we encounter this scenario, we run into a few performance impacting actions:

  • The currently loaded component is destroyed
  • Our new component is loaded into the view
  • The new component sends an HTTP request, which results in an error
  • The new component is destroyed
  • The previous component is reloaded and displays an error message

These actions are taken, with the DOM rendering and destroying multiple elements, which is an expensive set of operations. The nice thing is that Angular offers us a way to avoid dealing with these DOM actions: the Resolver.

The Resolver serves as intermediate code executed between the user clicking the link and your application loading the component. To see what I mean, check out the two different types of routing flow: one where we do not use a Resolver, and one where the Resolver has been implemented:

Standard Routing Flow:

  1. The user navigates to a new page by clicking a link
  2. The application renders the respective component

Resolver Routing Flow:

  1. The user navigates to a new page by clicking a link
  2. The application runs specific code and returns an observable
  3. The returned observable is stored in our new component, either in the constructor or ngOnInit()
  4. The component is loaded

The cool thing about Resolvers is that if we receive an HTTP error from our observable, the Resolver will not load our component; this prevents the DOM from going through the process of destroying and rendering all of these different elements. To implement a Resolver, check out the code below:

app-routing.module.ts

const routes: Routes = [
    { path: 'home', component: HomeComponent },
    { path: 'mock', component: MockComponent, resolve: { mock: MockResolverService } }
];

mockresolver.service.ts

export class MockResolverService implements Resolve<any> {
    constructor(private dataService: DataService) {}

    resolve(route: ActivatedRouteSnapshot): Observable<any> {
        return this.dataService.getAPIData().pipe(
            catchError(error => { return of('No data returned...'); })
        );
    }
}

mock.component.ts

export class MockComponent implements OnInit, OnDestroy {
    mockData: any;
    mockSubscription: any;
    constructor(private activatedRoutes: ActivatedRoute) {}

    ngOnInit() {
        this.mockSubscription = this.activatedRoutes.data.subscribe((response: any) => {
            this.mockData = response;
        });
    }

    ngOnDestroy() {
        this.mockSubscription.unsubscribe();
    }
}

If you'd like to learn more about Resolvers and everything else they can do, check out Angular's documentation.

Optimizing Change Detection with ChangeDetectionStrategy.OnPush

Change Detection (CD) automatically recognizes when changes happen in the application through manual triggers or asynchronous events. Once a change is detected, it iterates through the various components and triggers a refresh. Though this is usually very fast, it may trigger a lot of computations in larger applications, blocking the main browser thread.

Thankfully, Angular has given us the ability to set which components we change detection on through ChangeDetectionStrategy.OnPush. This will tell Angular to skip over these components during CD, unless:

  • The @Input() reference changes
  • An event originated from the component or one of its children
  • CD is run explicitly on the component through componentRef or markForCheck()
  • Async pipe is used in the view

Setting the ChangeDetectionStrategy is very simple and is done within a component's decorator:

@Component({
    selector: 'app-mock',
    templateUrl: './mock.component.html',
    styleUrls: ['./mock.component.css'],
    changeDetection: ChangeDetectionStrategy.OnPush
})

Now, whenever another component is loaded on the same page as the mock component changes, the application will go through and refresh all of the components loaded onto the page except the mock component. The only time that the mock component will be refreshed is if anything changes or updates on our mock component.

Reducing DOM Elements with FlexGrid Virtualization

The primary purpose of FlexGrid is to convert JavaScript objects into DOM elements that the user can interact with. In many instances, this data consists of hundreds, thousands, or even millions of rows of data; creating DOM elements for each of these items can be highly resource-heavy, causing slow and bloated pages.

Virtualization is the process of keeping track of which portions of the data are visible to the user and only rendering those sections in the DOM. This dramatically reduces the number of DOM elements in the document tree and improves performance, especially when working with extensive data sets.

Wijmo exposes the visible part of the data through its viewRange property; whenever the user resizes the screen or scrolls the grid, the viewRange gets updated, which updates the DOM. To prevent the number of elements in the DOM from ballooning, FlexGrid takes the cells that scroll out of the viewRange and recycles them, removing from the data that they were storing and repopulating them with the new data that is coming into the viewRange. This keeps your DOM lean and your application fast and lightweight.

We can see this demonstrated in this sample:

sample

As you can see, there are currently 100 rows of data in the grid and only 60 cell elements being rendered by the DOM. We get this number by using the following code:

flexgrid.updatedView.addHandler((s, e) => {
    this.rowCount = s.rows.length.toString();
    this.cellCount = s.hostElement.querySelectorAll('.wj-cell').length.toString();
});

The s.hostElement.querySelectorAll('.wj-cell') method returns the an array of elements rendered in the DOM that have the .wj-cell class appended to them. As we scroll down the grid, we see that the number of rows of data within FlexGrid increase, but the number of cell elements stay the same:

grid

Lazy Loading Angular Modules

Angular uses Webpack to create JavaScript bundles when your application compiles, which are then sent over to the client. By default, Angular loads all of your modules as soon as the application loads, even if all of these modules are not necessary for the page the client is loading. With larger applications, the size of the application increases significantly, which means that the size of your bundles will also increase.

As the size of the main bundle increases, your application's performance will decrease because the main bundle is contributing to:

  • Slower downloads
  • Slower parsing
  • Slower JavaScript execution

For larger applications with many routes, developers should consider implementing lazy loading - a design pattern that loads modules as needed. Lazy loading helps keep initial bundle sizes smaller, which helps decrease load times.

You'll need a feature module with a component to route to implement lazy loading modules. To generate a new NgModule, run the following command in the terminal:

ng generate module mock --route mock --module app.module

This creates a mock folder having the lazy-loadable feature module MockModule defined in the mock.module.ts file, and the routing module MockRoutingModule defined in our mock-routing.module.ts file. The command also automatically declares the MockComponent that we created earlier and imports MockRoutingModule inside the new feature module.

Because the new module is meant to be lazy-loaded, it does not add a reference to the new feature module in your application's app.module.ts file; instead, it adds the declared route to the routes array. Our app-routing.module.ts file now looks like this:

const routes: Routes = [
    { path: 'home', component: HomeComponent },
    { path: 'mock', component: MockComponent, resolve: { mock: MockResolverService } },
    { path: 'mock', loadChildren: () => import('./mock/mock.module').then(m => m.MockModule) }
];

Now we have two different routes that reference our Mock component. Don't worry, though! Angular supports using Resolvers along with lazy loading; all we need to do is combine these two different route objects into one:

const routes: Routes = [
    { path: 'home', component: HomeComponent },
    { path: 'mock', loadChildren: () => import('./mock/mock.module').then(m => m.MockModule), resolve: { mock: MockResolverService } }
];

When our application loads, our mock module will not load with the main bundle. Instead, our application will wait until a user navigates to the mock path to load this module, decreasing the initial load time of our app and decreasing our primary bundle size.

Conclusion

With all of these tools at your disposal, you'll now be able to reduce the amount of work that Angular has to perform when users interact with your application, as well as lighten the load of the browser's DOM when it comes to rendering and destroying components. Happy coding!

Download Now!


Joel Parks - Product Manager

Joel Parks

Technical Engagement Engineer
comments powered by Disqus