Improve LCP on angular app

Intro

Things that slow a web application are:

  • requests
    • Ressources loading (JS / CSS / images)
    • API calls
  • Files size
    • JS / CSS files
    • Medias (images / videos)
  • Rendering
    • Javascript execution
    • CSS parsing
    • DOM construction

In this listing there is some things we can do to improve the performance of our application, so improve the LCP.

In this article I will assume the application is already doing SSR (Server Side Rendering).

Requests

Ressources loading

CSS inline

The first thing that is pretty easy to do on angular is to make the css loading inline. This mean the css will be in the DOM, so when the server is sending the HTML to the client, the CSS is already there. So the browser will not have to make a new request to get the CSS file(s).

To do this, you can use the inlineCritical option in angular.json file.

{
  "configurations": {
    "production": {
      "optimization": {
        "styles": {
          "inlineCritical": true // <-- add this line
        }
      }
    }
  }
}

Lazy loading

Some resources are not needed for the first render. Because they are not visible, or not mandatory.

For example, picture below the viewport. Or some widget from a third party.

These resources can be lazy loaded.

In angular there is multiple ways to do it.

There is a @defer instruction that can be used in template. You can learn more about it here.

NB: Before angular 19.2 @defer can block the stable state of the application and cause unintended behavior.

You can see some details on an angular repo issue

For the pictures you can use the ngSrc directive. This will lazy load the image when it is in the viewport.

Wait for the LCP to be done

If you want to run something after the LCP is done, you can wait for the application to be stable. It not exactly the LCP, but it is close enough.

This would be done like this :

constructor(
    private appRef: ApplicationRef, 
    private cd: ChangeDetectorRef
  ) {}

  ngOnInit() {

    this.appRef.isStable.pipe( first((isStable) => isStable) ).subscribe((isStable) => {
     // do something after the LCP is done
       this.cd.markForCheck();
    });

  }

You will need to mark the component for check, because the isStable observable is ran outside zone.js.

When the is stable is trigger you can run your third party code or whatever you want.

API calls

  • Caching with redis
  • Ran SSR
  • Browser state
  • limit the number of API calls
    • if you have multiple API calls maybe merge them into one by changing the backend
    • if things don’t need to be updated frequently, it’s not necessary to make it changeable in an admin panel, they could be hardcoded.

Files size

JS

  • Making smaller bundle
    • Standalone components
    • Lazyloaded routes
    • defer not needed components
    • import/provide only what you need
  • lighthouse bundle analyze

When the application load all the files if the javascript files are heavy it will be slower. So we need to have to make them as small as possible. In angular there is multiple steps.

When angular is building the application, it create bundles files. These files are containing all of the javascript code needed to run the application. If you don’t do anything, all your components and module will be included. In the recents version of angular, there is a new feature called standalone components. This is a way to create components that are not part of a module. This means that you can use them without having to import the module. In addition to that, if you lazy load your routes, this means that when you load your application, the bundle will only contain the route components and everything that the component needs. Others routes components will be loaded when the user navigate to the route.

In the standalone components, you need to import only the things you need. For example if you need a Button from a library, if it’s possible you should only import the ButtonComponent and not a full module that would include other things that you won’t necessarily use.

An other way to reduce the size of the bundle is to use the defer instruction. This will not include the defered component in the initial bundle, so the additional bundles will be loaded when the defer is triggered.

For example if you have a third party that is loading a lot of things. You can move all the code that trigger the third party to a new component.

Then you can use the defer instruction to load the component later. So all the code won’t be triggered. Also if you have a component that use heavy library, and this component is not mandatory for the initial page, defering it will make a much smaller bundle.

Sometime also removing or replacing some libraries can help. For example if you have momentjs that is use only one time in the component, maybe using an alternative or using the native javascript can help.

To determing what is included in the bundle you can run the lighthouse using chrome developer tools.

Then there is a treemap button that will show you the size of each file in the bundle.

treemap button

If the name are not clear, you can change you angular.json configuration locally to disable some options. You can put it in the production configuration, and then run you app in the production locally to run the lighthouse. Just make sure to not push it to production, keep it only locally during you tests.

{
    "production": {
      "namedChunks": true,
      "sourceMap": true,
      "outputHashing": "none"
    }
}

Media

The size of the medias are also important. If you can change the size of the image according of the height and width of the image on you page. Also you can compress the image. There is websites that work well for that like tinypng. You can also use the webp format. This is a new format that is usually better than the jpeg or png format. For the videos the equivalent format is webm.

Rendering

By removing unused CSS and JS, we can reduce the amount of work the browser has to do to render the page. Also by reducing all the files as explained above, it’s less code for the browser to interpret, so the rendering will be faster.

Sources