Some optimization for an angular app

Explanation

Recently I was working on a project that was using Angular 15 and there was some issues with the performance of the app, there was a feeling that everything was slow, and the browser was laggy, and I needed to find a way to make it faster.

Here is the list of things I did to improve the performance of the app.

SSR

Doing the rendering on the server side will improve the performance of the app, because the user will get all the DOM at once, and will not have to wait for the javascript to be loaded and executed. It will also improve the SEO of the app, because the search engine will be able to see the content of the app.

The Http calls can also be done by the server, which should be faster than done from the client side, because usually the server is closer to the backend server and also have a better internet connection.

I won’t go into the details of how to setup SSR, but here is a link to the official documentation of Angular: https://angular.dev/guide/ssr

Lazy Loading

Modules

When you access a page of the app by default, all the modules of the app are loaded. On large application this can be a lot of javascript. And all the modules are not needed.

For example, when you go to the home page, you don’t need the javascript of the connected pages, like the profile page or even the admin parts.

So it possible to do lazy loading depending on the route. While you navigate through the app, the modules will be loaded when needed.

Usually the lazyloading looks like this:

const routes: Routes = [
    {
        path: 'admin',
        component: AdminRootComponent,
        loadChildren: () =>
            import('./admin/admin.module').then((m) => m.AdminModule),
    },
    {
        path: '',
        component: PublicRootComponent,
        loadChildren: () =>
            import('./public/public.module').then((m) => m.PublicModule),
    },
];

Here is the documentation: https://angular.io/guide/lazy-loading-ngmodules

Images

When you load a page you first get the DOM, and then the browser will render the page, and load the js files, css files, and while parsing the HTML if there is some image they will be loaded.

If your page contains a lot of image and has a lot of content, some images are not visible at first, but they will be loaded anyway.

To avoid this it’s possible to use the loading="lazy" attribute on the image tag. This will tell the browser to load the image only when it’s visible on the screen.

Another way to do it is to use an angular directive that will do everything fo us. It will add the loading="lazy" attribute on the image tag, and it will also add prefetch hint if needed.

Also it’s important to have light images with good compression and the right dimension. Usually svg or webp are the best format for the web.

Data loading

One of the main problem I found was about the data loading. Here is the list of problems I found:

  • Too much http calls
  • More information than needed (properties that are not used)
  • Too much entities loaded at the same time and the pagination done client side

Number of http calls

On some pages there was a lot of http calls to load all the needed data.

For example, on the home page we needed to load 3 reviews, 3 blog posts, the lower price of the products, 3 items that explain how the app works and finally we had an SEO content that was coming from a WYSIWYG editor. We also had some calls for the app globally, like links in the header, in the footer, average rating in the navbar.

So if we count all the calls we have 5 calls for the home page, and 4 calls for the app globally.

To reduce the number of http calls, we can do some changes on the backend, like creating a route that will return all the data needed for the home page, and another route for the app globally.

And of course for the global data, we don’t need to re fetch the data on each page, so we can store the data in a store like redux, or in the local storage.

Quantity of informations

Sometimes we get entities, but we don’t need all properties of these entities.

For example, still on the home page, the review entity is linked to a product, the product has an history of changes like prices and other things. But on the home page we only need the rating, the comment and the name of the product.

So a good change was to return only the needed properties of the entities. For this you will need to change the backend if this is currently not possible to filter the properties.

Number of entities loaded

In some case you have a lot of entity in your database, so you don’t want to show thousands of entities on the same page, so you will need to paginate the data.

By loading all the data at first and doing the pagination on the client side you will reduce http call, it can work for small amount of data, but it’s not a good solution, and it won’t scale.

The best solution is to do the pagination on the backend, and only load the data that will be displayed on the page.

On our home page we were showing 3 reviews, so we only needed to load 3 reviews, and not all the reviews of the app. And if needed you can add buttons to load more reviews, and do a http call with an offset and a limit to load the next reviews.

Another example is the average rating in the navbar, in my opinion it’s better to get the average rating from the backend (width an SQL query for example) than loading all the reviews and doing the average on the client side. Because it will be a lot of entities to serialize for the backend and then to deserialize for the client side, and then to do the average.

Data loading conclusion

Each problem individually is not a big deal, but when you have all of them on all the pages of the app, it can be a big difference. Depending on the app, if it’s a new app or an existing one, if you have access to the backend or not, and the framework used, the solution will be different, and everything won’t be possible. The goal is to keep in mind that each http call is a cost, and you need to find the best solution for your app.

Note: I didn’t talk about caching, but that could be a solution to reduce the number of http calls.

Observables

When you subscribe to an observable, if you don’t unsubscribe it, it will stay forever in memory and sometimes it will still trigger the actions.

This include the observables form the forms, like the valueChanges observable.

There is some exceptions, like Http calls that will be unsubscribed automatically.

It won’t hurt if you unsubscribe one of the exception, but it’s not necessary.

To automatically unsubscribe the observables, you can use the takeUntil operator. This operator will take an observable as parameter, and when this observable emit a value, it will unsubscribe the observable. Here is an example:

export class ExampleComponent implements OnDestroy {
    exampleObservable!: Observable;
    private destroy$ = new Subject();

    constructor() {
        this.exampleObservable
            .pipe(takeUntil(this.destroy$))
            .subscribe(() => {
            // do something
        })
        
    }
    
    ngOnDestroy(): void {
        this.destroy$.next(null);
        this.destroy$.complete();
    }
}

Here is some links questions that explain it better than me:

Data stored in the browser

When you retreive data from the backend, you can store it in the browser, in a store like redux, or in a property of a service or a component.

I am talking about data that are not removed from memory, for whaterever reason when you navigate through the app. For example if the data is shared between multiple pages and components.

If you don’t need the data anymore, and it’s a large amount of data, it could be a good idea to remove it from the memory, but it will depend on the use cases and the context.

But keeping all the data in memory can be a problem, because the browser will have to keep all the data in memory, and it will take some space, and it could slow down the app.

SSR Caching

With SSR some pages can still be slow, depending on the content of the pages, the number of http calls, the number of entities loaded.

To remedy this, you can use caching, the first time the page is loaded, the server will cache the page once rendered, and the next time the page is loaded, the server will return the cached page, and it will be faster.

You should do this only for pages that are static content and that are public, because if the page is dynamic, the user will see the same content as the first time he loaded the page. Also if the page is restricted to some users, the cache will be the same for all the users.

If the pages changes sometimes you can set a TTL (Time To Live) for the cache, and the server will return the cached page until the TTL is reached, and then the server will render the page again.

It’s also possible to warm up the cache, by loading the pages in the background, so the user will always get the cached page.

sources / more information: https://www.camelcodes.net/how-to-make-angular-universal-ssr-load-faster/ https://santibarbat.medium.com/4238-faster-pages-how-to-cache-angular-universal-routes-217c86c1b673

Delay 3rd party scripts

Usually the 3rd party scripts are not required for the page to be usable, so it’s possible to delay the loading of these scripts. In my case I wanted them to be launched after the LCP, to reduce the impact on the initial loading time.

I had a component that was loading the 3rd party scripts, and I was using the ngOnInit. The best way I found was to run the scripts inside a setTimeout with a delay of 0ms, so the scripts will be launched in the next detection cycle. All other lyfecycles hooks were not working as expected, because the scripts were launched before the LCP. This was something explained by an angular developer in a talk, here is the youtube link: https://www.youtube.com/watch?v=IY-QOz4oLCE

Sources

https://benlesh.medium.com/rxjs-dont-unsubscribe-6753ed4fda87 https://stackoverflow.com/questions/41364078/angular-2-does-subscribing-to-formcontrols-valuechanges-need-an-unsubscribe https://www.learnrxjs.io/learn-rxjs/operators/filtering/takeuntil

https://tiwarivikas.medium.com/understanding-memory-leaks-in-angular-a-concise-guide-with-examples-61cc3e4aaa49