DEV Community

Cover image for How I optimized my Angular website
Abdjalil Brihoum
Abdjalil Brihoum

Posted on • Updated on

How I optimized my Angular website

Introduction

Building applications/websites using Angular has a downside - the bundle size. This directly affects the loading speed and user experience of our projects.

Angular bundle size

Reducing the bundle size is important, but there are other essential elements to consider for creating an ideal website.

Personally, I follow a four-step process when building apps/websites:

  1. Designing
  2. Coding
  3. Ensuring Responsiveness
  4. Optimizing

In this post, we will focus on the last step.


Getting started

I'll start by discussing the problems I encountered and how I addressed them, including the steps I took to reduce the bundle size.

1. Visual Problems

The following link showcases my website after the 3rd step.

website

From this video, I can identify 3 visual problems :

1.1. Visual Problem 1

The website looks broken for a split second, then loads normally.

Angular styles late loading

1.2. Visual Problems 2 & 3

The font took ages to load, the same thing goes with the pizza picture and the other more important resources

No font for the pizza picture


2. Invisible problems

Let's open the dev console and see what's happening under the hood.

I can identify two issues from this video.

2.1. Invisible Problem 1

dev-tools

The website took 6.84s to fully load, with 109 requests and 4.4 MB of resources. This happened because the website loaded all its resources from pages 1 to 5, including unnecessary ones.

To put those numbers into perspective, it would take a 3G internet connection about ~24s to load all the resources.

2.2. Invisible Problem 2

bad loading tree

The website is loading resources that are not needed initially, before loading the necessary ones, causing a delay in rendering critical resources.

For example :

  • The website first loaded all the used backgrounds, from page 1 to 5 (number.3).

  • Then, a picture located on page 4, which was not yet needed, was loaded (number.4).

  • Next, 7 other pictures located on page 2, also not yet needed, were loaded (number.5).

  • Lastly, the most critical resource, the pizza picture was loaded.

So, the pizza picture took approximately ~2.129s (1.92s + 0.209s) (number.1 & 2) to load, resulting in a broken slider being displayed to the user during this time.

The same goes for the font.

late font loading

It was the last resource to load, taking approximately ~4.09s (1.72s + 2.37s) to render.

Note:

You may ask: Why did he pick the pizza picture from all the pictures ❓

I'll answer you with another question :

What picture do you see first when you visit the website ❓


Lighthouse Score

Lighthouse Score

As expected, the LCP (largest Contentful paint) and the CLS (Cumulative Layout Shift) are bad, due to the Invisible Problem 2 and Visual Problem 1 respectively, surprisingly the FCP (First Contentful Paint) is decent.

Bundle size

Bundle size

We can do much much better.

Before explaining and fixing said problems, let's first optimize the bundle size.


Improving the bundle size 📉

I would like to highlight something before starting :

  • ⚠️ Never import third-party styles (CSS/Sass...) inside any of your Angular components, instead use the global styles.scss file.

There are many ways to reduce the bundle size, but that's not the focus of this article, here I will be showcasing how 'I' optimized my Angular website.

1. Lazy loading third-party libraries

The first thing I personally do is to Lazy Load noncritical third-party libraries. This means libraries that are not required the second the website loads, therefore their load can be delayed until all the more important resources are loaded and rendered. This reduces the main bundle size, which in turn improves the website's loading speed.

I'll give some examples to clarify more :

  • I have a plugin called lightGallery, which is only needed when a user wants to open an image gallery. Logically, its load can be delayed until the more critical resources of the website (such as the pizza picture and important styles/fonts) are downloaded and rendered in the view.

  • I also have Bootstrap installed. its JavaScript is only required when we need interactivity in our project, like for example: opening a modal, Using a collapse, a carousel… So we can delay its load too.

That's the list of non-critical third-party libraries used by the website :

Here, in the video below, I'll explain how to lazy load them.

⚠ Note: I made a small mistake on the video (1:30), it should be

"input": "node_modules/wow.js/dist/wow.js",

&

appendScript('wow.js');

ℹ The code I used on the video :

// app.component.ts
function appendScript(name: string) {
  let script = document.createElement("script");
  script.src = name;
  script.async = true;
  document.head.appendChild(script);
}
Enter fullscreen mode Exit fullscreen mode

To summarize 📝 :

  • We instructed Angular to load our scripts as separate files during the build and not inject them, so they can be lazy-loaded.

  • Then, we used window.onload event to load our scripts after the entire website, including its content such as images and styles, has loaded. This approach ensures that our scripts are the last resources to load.

1.2. Bundle size

Bundle size 1

✅ Just like that, we eliminated (163.02 kB).

2. Removing unused modules

This website has no routes, as it only contains a single page, even though the RouterModule is installed. We need to remove it because it's basically useless.

On the app.module.ts file, we remove RouterModule from the imports array:

app.module.ts

2.1. Bundle size

Bundle size 2

✅ We were able to eliminate (75.85 kB) by removing RouterModule.

This means we eliminated a total of (238.71 kB) from the initial build.

▶ You can check the website after reducing the bundle size.

website

Lighthouse Score

The website's Lighthouse score has slightly improved, but all the previously mentioned issues still persist. Therefore, we need to address them to optimize the website's performance further.

Lighthouse


Explaining Visual Problem 1

This website uses Bootstrap and styles.css contains Bootstrap's CSS and other important styles. The reason for this problem is that Angular started rendering the website before styles.css finished downloading, causing the page to be displayed without the necessary styles.

styles.scss

To verify this, we can test by blocking the download of styles.css completely and observe if we still get the same results :

no styles.css

Yes, we do.


Solving Visual Problem 1

To solve this issue, we need to ensure that all critical CSS is loaded before Angular starts rendering the website.

Critical CSS means :

The CSS responsible for the content that's immediately visible when we open a website.

or :

The CSS of the first page you see when opening a website.

How can we know ❓ well, watch the video below :

So, we have as critical the following :

  • Bootstrap CSS;
  • SwiperJS CSS;
  • Custom CSS;
  • Animations;

⚠ Note: The font file is also critical, however, we will address it with Visual Problems 2 & 3. This is because the font file contains 3 fonts, whereas the homepage requires only 1. For now, we consider it non-critical.

And as non-critical :

  • Snackbar CSS
  • lightGallery CSS & its plugins
  • fonts file

Now, Instead of using a single file (like styles.scss) containing all styles, we can separate them based on their priority.

We create two files: pre_styles.scss & late_styles.scss and import our critical and non-critical styles into them, respectively.

late & pre styles

If the .gif is not clear, that's the content of the two files (styles.scss is completely empty):

late & pre styles

ℹ Note: Instead of importing the entire Bootstrap library, you can import only the specific components that you need.

Now, I need to preload pre_styles.scss & lazy load late_styles.scss.

1. Preloading critical styles

Just like we did before when we lazy loaded third-party libraries javascript, we need to tell angular to load pre_styles.scss as a separate file during the build and not inject it, so we can preload it.

Inside angular.json, under the styles[] array, we remove the default value :

// angular.json
"styles": ["src/styles.scss"],
Enter fullscreen mode Exit fullscreen mode

and change it to :

// angular.json
  "styles": [
    {
      "input": "src/pre_styles.scss",
      "inject": false
    },
    {
      "input": "src/late_styles.scss", // <- we will use it later.
      "inject": false
    }
  ]
Enter fullscreen mode Exit fullscreen mode

angular.json separate file

After we build, we can locate our two files, pre_styles.scss and late_styles.scss, inside the dist/ folder.

styles

Now, in order to preload pre_styles.scss, we add the following code inside the <head> element of src/index.html:

<!-- index.html -->
<head>
  ...
  <link rel="preload" href="pre_styles.css" as="style" />
  <link rel="stylesheet" href="pre_styles.css" />
</head>
Enter fullscreen mode Exit fullscreen mode

preload styles

What I've done here is prioritize the loading of pre_styles.css as the first resource in the download queue. This means the browser will start downloading the website resources with pre_styles.css at the top of the list so that when Angular starts rendering, the critical styles are already loaded and ready.

You can read more about rel=preload : https://developer.mozilla.org

2. Lazy loading non-critical styles

In the previous step, we already told angular to load late_styles.scss as a separate file, it's time to use it.

Within the ngAfterContentInit() method of app.component.ts, we define a new function named appendStyle() below the appendScript() function that we created earlier :

// app.component.ts
ngAfterContentInit() {
  //...
  function appendScript() {
    //...
  }
  function appendStyle(name: string) {
    let style = document.createElement("link");
    style.rel = "stylesheet";
    style.type = "text/css";
    style.href = name;
    document.head.appendChild(style);
  }
}

Enter fullscreen mode Exit fullscreen mode

Now, we use appendStyle() function to lazy load late_styles.scss by calling it within the window.onload event :

// app.component.ts
  ngAfterContentInit() {
    //...
    window.onload = () => {
      //...
      appendStyle('late_styles.css');
    };
  }
Enter fullscreen mode Exit fullscreen mode

lazy loading styles

ℹ A reminder: The window.onload event is fired when the entire website loads.

Now, we restart the server and observe :

order of resources

We can see that pre_styles.css is the first file to load, providing all the critical styles necessary for Angular to begin rendering, eliminating the broken website look.

late_styles.css is among the last files to load, making room for more critical styles to load faster.

✅ We completed our task, and Visual Problem 1 is now fixed.

❗ However, we may have a tiny problem :

large file size

pre_styles.css is quite large, and a significant portion of it contains dead code that is never used. This is mainly because we imported the entire Bootstrap library instead of selectively importing the required components.

✅ Using purgeCSS, we can eliminate all the unused code and optimize the performance further.

3. Installing PurgeCSS

On the command prompt :

# command prompt
npm i -D purgecss
Enter fullscreen mode Exit fullscreen mode

Now, we create a new file named purgecss.config.js on the root of the project, and add the following code :

// purgecss.config.js
module.exports = {
  content: ["./dist/**/index.html", "./dist/**/*.js"],
  css: ["./dist/**/combined.css"],
  output: "./dist/[FOLDER]/combined.css",
  safelist: [/^swiper/],
};
Enter fullscreen mode Exit fullscreen mode

⚠ Note: Replace [FOLDER] with your app name (within the output property).

purgeCSS with angular

ℹ Note: You may have noticed that I have set the safelist to [/^swiper/]. This is because I want to prevent PurgeCSS from removing any CSS related to SwiperJS. Since SwiperJS adds CSS classes dynamically after the page load, PurgeCSS may not be aware of them and could mistakenly remove them.

Next, we navigate to package.json and create a new script named purgecss :

"purgecss": "purgecss -c purgecss.config.js",
Enter fullscreen mode Exit fullscreen mode

Then, we edit the build script from :

"build": "ng build"
Enter fullscreen mode Exit fullscreen mode

To :

 "build": "ng build && npm run purgecss "
Enter fullscreen mode Exit fullscreen mode

package.json

⚠️ Note: Build using npm run build instead of ng build, so purgecss script kicks in.

After building, the size of pre_styles.css dropped by (190.6 kB) :

pre_styles.css purged

Lighthouse

Lighthouse 22

The CLS (Cumulative Layout Shift) has decreased, because, as expected, we fixed Visual Problem 1. However, LCP (largest Contentful paint) is still poor, because the pizza picture is always late to the party, AKA Visual Problems 2 & 3.

bad LCP

▶ Visit the website after this step :

website


Explaining Visual Problems 2 & 3

The cause of this problem is the order of resources in the download queue.
As you may already know, browsers have a limit on the number of parallel requests.

Chrome limit of requests

Source: blog.bluetriangle.com

Given this limitation, we must prioritize the order in which resources are loaded, making sure to load high-priority resources like the font and pizza picture before the lower-priority ones

Solving Visual Problems 2 & 3

This issue can be easily resolved by preloading the font and the pizza picture, as we did earlier.

Inside the <head> element of src/index.html, we add the following code :

<!-- index.html -->
<head>
  ...
  <link rel="preload" href="[YourPath]/pizza.webp" as="image" />
</head>
Enter fullscreen mode Exit fullscreen mode

And we follow the same procedure with the font used on the home page (DayburyRegular.woff2) :

<!-- index.html -->
<head>
  ...
  <link
    rel="preload"
    href="[YourPath]/DayburyRegular.woff2"
    as="font"
    type="font/woff2"
    crossorigin="anonymous"
  />
</head>
Enter fullscreen mode Exit fullscreen mode

index.html

⚠ Note: The use of crossorigin here is important.

Now, I need to transfer my font from fonts.css to pre_styles.scss

font

⚠️ Note: Make sure to use the correct path for the font src when transferring it.

load

✅ The task has been completed and the issue causing Visual Problems 2 & 3 has been resolved. This means no more delays in loading the font or the pizza picture.

▶ Visit the website after this step :

website


Explaining Invisible Problem 1

This issue occurs because the website loads all its resources from pages 1 to 5, even those that are not necessary.
Ideally, we should only load resources that are currently visible in the viewport and postpone the loading of the remaining resources until we scroll to them.
By following this approach, we accelerate the loading time of the visible resources by reducing the number of files that need to be downloaded initially.

ℹ️ TL;DR: We should only load resources that are visible on the screen.

Solving Invisible Problem 1

The solution to this issue is to use a library that loads only the visible resources and lazy loads the rest. My personal choice would be lazysizes By Alexander Farkas.

I would be happy to explain how I implemented lazy loading on my website using lazysizes, but this post is already long enough and that's not the primary focus for today. Instead, I will jump directly to the results. However, I am planning to create a detailed guide with step-by-step instructions, which I will link here for reference.


I implemented lazy loading for all the pictures and backgrounds on the website. The image below shows the difference before and after the implementation :

dev tools 2

✅ The website made only 49 requests initially, transferred 1.1 MB of resources, and loaded all of them in just 1.28s.

ℹ Note: The results mentioned above were achieved without utilizing the browser cache as it was disabled during the test.

And by caching the resources, the load time can be reduced to a maximum of ~ 0.5s.

dev tools 2 - cache

Now, resources will be loaded as we scroll to them :

load on scroll

✅ The issue causing Invisible Problem 1 has been successfully resolved.

▶ Visit the website after this step :

website

Lighthouse score

Lighthouse

Lighthouse report badge

Mission accomplished ✅.

The load is fast as ⚡.

ℹ️ Note: Invisible Problem 2 was also resolved when we fixed the Visual Problems 2 & 3 issues.

MORE SPEED!

What if I told you we can reduce the bundle size even more and enhance the already impeccable Lighthouse score ❓

✳ The method I'm about to show you will allow us to initially load only the first visible page, and then lazy load the remaining pages afterward.

This means that instead of loading the entire website, we only load the first page component, resulting in a smaller initial page size and faster loading speed.

ℹ️ TL;DR: We only load components that are currently in view, and lazy load the remaining.

⚠ Note: Keep in mind this method comes with several downsides.


To start, Inside my app.component.ts, I need to remove all pages except for page 1 from rendering, meaning only page 1 can be loaded and displayed.

After that, I need to add a new <ng-container> and declare a template Variable named #injectHere inside it.

lazy load components

Next, in order to access <ng-container>, we declare the following :

@ViewChild('injectHere', { read: ViewContainerRef }) injectHere!: ViewContainerRef;
Enter fullscreen mode Exit fullscreen mode

And below, we define a new method that will be responsible for loading the remaining components :

  async loadComponents() {
    const { Page2Component } = await import('./components/page2/page2.component');
    this.injectHere.createComponent(Page2Component);
    const { Page3Component } = await import('./components/page3/page3.component');
    this.injectHere.createComponent(Page3Component);
    const { Page4Component } = await import('./components/page4/page4.component');
    this.injectHere.createComponent(Page4Component);
    const { Page5Component } = await import('./components/page5/page5.component');
    this.injectHere.createComponent(Page5Component);
  }
Enter fullscreen mode Exit fullscreen mode

Now, we need to call this method. In my opinion, it is better to wait for page 1 to fully load before loading the remaining components to ensure a faster initial loading speed. To achieve this, we call the method under the window.onload event :

window.onload = async () => {
  await this.loadComponents();
  //...
};
Enter fullscreen mode Exit fullscreen mode

ℹ Note: loadComponents() is asynchronous and should be awaited before proceeding further.

lazy-load-components

And if we open the dev-console :

angular lazy load components

▶ Visit the website after this step :

website

Bundle size

bundle size

✅ The main bundle size was reduced by (83.29 kB), resulting in a faster loading time and quicker display of page 1.

Lighthouse Score

lighthouse score

Lighthouse report badge

We have achieved a slight improvement by gaining some milliseconds and completely eliminating the blocking time.


The Takeaways

  • Make sure that all of your critical styles are ready when your website starts rendering.

  • If necessary, preload some of your main resources, like we did with the pizza picture and the font.

  • Always, like always lazy load your images, and if possible, lazy load your non-critical JS & CSS.

  • Install the minimum required third-party libraries and uninstall any unused ones.

  • Always open your dev console, analyze and prioritize the order of your resources.


If you have suggestions or advice regarding the information I shared, please don't hesitate to let me know. Your feedback is always welcome.

I aimed to explain everything in a beginner-friendly way, which may have led to some repetition.

You can find the website's source code on Github if you're interested:

MegaPizza Github

Oldest comments (6)

Collapse
 
alaindet profile image
Alain D'Ettorre

Amazing content, thank you very much

Collapse
 
retry2z profile image
Hristiyan Hristov

Can you show us the mobile tests cz there I face more problem in optimizing already at 100 in desktop.

Collapse
 
brihoum profile image
Abdjalil Brihoum

Lighthouse on mobile simulate a mid-tier mobile (Moto G4), a 6 years old phone with 2gb of ram & a Snapdragon 617.

And as we know, Websites under Angular renders in the client side, means rendering pages directly in the browser using JavaScript. And because the simulated phone is mid-tier, it takes time to download, execute, and render the page. The solution for your problem is to implement server side rendering. Google Angular universal to know more.

Collapse
 
retry2z profile image
Hristiyan Hristov

Yea got SSR even more split desktop and mobile versions to clean code what I send to user I make very nice interesting approach to handle this. And still I see around 85+ for performance and thinking whaaat I can do more.

Collapse
 
zohar1000 profile image
Zohar Sabari

That's a tremendous work, it was facsinating to read. I saved the link, kudos!

Collapse
 
shivams1007 profile image
Shivam Singh

Amazing blog really helpful!