Introduction
Building applications/websites using Angular
has a downside - the bundle size. This directly affects the loading speed and user experience of our projects.
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:
- Designing
- Coding
- Ensuring Responsiveness
- 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.
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.
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
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
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
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
.
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
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
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);
}
To summarize 📝 :
We instructed
Angular
to load our scripts as separate files during the build and not inject them, so they can belazy-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
✅ 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:
2.1. Bundle size
✅ 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.
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.
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.
To verify this, we can test by blocking the download of styles.css
completely and observe if we still get the same results :
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.
If the .gif
is not clear, that's the content of the two files (styles.scss
is completely empty):
ℹ 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"],
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
}
]
After we build, we can locate our two files, pre_styles.scss
and late_styles.scss
, inside the dist/
folder.
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>
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);
}
}
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');
};
}
ℹ A reminder: The window.onload
event is fired when the entire website loads.
Now, we restart the server and observe :
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 :
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
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/],
};
⚠ Note: Replace [FOLDER]
with your app name (within the output
property).
ℹ 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",
Then, we edit the build
script from :
"build": "ng build"
To :
"build": "ng build && npm run purgecss "
⚠️ 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) :
Lighthouse
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.
▶ Visit the website after this step :
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.
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>
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>
⚠ Note: The use of crossorigin
here is important.
Now, I need to transfer my font
from fonts.css
to pre_styles.scss
⚠️ Note: Make sure to use the correct path for the font src
when transferring it.
✅ 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 :
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 :
✅ 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
.
Now, resources will be loaded as we scroll to them :
✅ The issue causing Invisible Problem 1 has been successfully resolved.
▶ Visit the website after this step :
Lighthouse score
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.
Next, in order to access <ng-container>
, we declare the following :
@ViewChild('injectHere', { read: ViewContainerRef }) injectHere!: ViewContainerRef;
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);
}
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();
//...
};
ℹ Note: loadComponents()
is asynchronous and should be awaited before proceeding further.
And if we open the dev-console :
▶ Visit the website after this step :
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
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 thefont
.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:
Top comments (6)
That's a tremendous work, it was facsinating to read. I saved the link, kudos!
Amazing content, thank you very much
Can you show us the mobile tests cz there I face more problem in optimizing already at 100 in desktop.
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.
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.
Amazing blog really helpful!