Improve LCP on an Angular app

Intro

Things that slow a web application are:

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

In this list there are some things we can do to improve the performance of our application, and therefore the LCP.

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

Requests

Resources loading

CSS inline

The first thing that is pretty easy to do on Angular is to make the CSS load 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, pictures below the fold. Or some widget from a third party.

These resources can be lazy loaded.

In Angular there are 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’s 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 run outside zone.js.

When isStable emits 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 loads all the files, if the JavaScript files are heavy it will be slower. So we need to make them as small as possible. In Angular there are multiple steps.

When Angular is building the application, it creates bundle 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 modules will be included. In the recent versions 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. Other route components will be loaded when the user navigates 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.

Another way to reduce the size of the bundle is to use the defer instruction. This will not include the deferred 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 uses a heavy library, and this component is not mandatory for the initial page, deferring it will make a much smaller bundle.

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

To determine what is included in the bundle you can run Lighthouse using the 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 names are not clear, you can change your angular.json configuration locally to disable some options. You can put it in the production configuration, and then run your app in production locally to run Lighthouse. Just make sure to not push it to production, keep it only locally during your tests.

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

Media

The size of the media is also important. If you can, change the size of the image according to the height and width of the image on your page. Also you can compress the image. There are 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