Introduction
Building applications/websites with Angular
always comes with a downside: the bundle size.
The latter has a direct impact on the loading speed & the user experience of our projects.
Even if we finally reduced the bundle size, there are other boxes to check to have the ideal website.
Personally, I have four steps to follow when building apps/websites.
- Designing
- Coding
- Making the website responsive
- Optimizing
On this post, we will focus on the last step.
How I optimized my Angular website
I'll start with the problems I've faced, then how I've addressed them.
1 - Visual Problems
The following link is a showcase of my website after the 3rd step.
From this video, I can extract four visual problems :
1.1 - Visual problem 1
The website looks broken for a split second, then loads ordinarily
1.2 - Visual problem 2 & 3
The font took ages to load, same thing goes with the pizza picture
1.3 - Visual problem 4
The speed of loading images is super slow.
2 - Invisible problems
Let's open the dev-console and see what's happening under the hood.
I can take out two issues from this video
2.1 - Invisible problem 1
The website took 4.57s to fully load, with 98 requests and 5.4 MB of resources. To put those numbers into perspective, a 3g internet will take about ~24s to load all the resources.
2.2 - Invisible problem 2
The pizza picture took ~1.07s (0.689s + 0.387s) to be displayed, this means the user was seeing a broken slider for 1 second. The same goes with the font.
You may ask : Why did he pick the pizza picture from all the other pictures ?
I'll answer you with another question :
What picture do you see first when you visit the website ?
Lighthouse Score
As I expected, the LCP (largest Contentful paint) and the CLS (Cumulative Layout Shift) are bad, because of Invisible problem NΒ°2 and Visual problem NΒ°1 respectively, surprisingly the First Contentful Paint is good.
Bundle size
Not that bad, but we can do better.
βΉοΈ Note : before explaining and fixing said problems, lets first optimize the bundle size.
Improving the bundle size
Before starting, I would like to highlight something :
- β οΈ Never import third party CSS inside any of your Angular components, instead use
styles.css
.
There are lots of ways on how to reduce the bundle size, but that's not the subject for today, here I'm showcasing how 'I' optimized my Angular
website.
1 - Lazy load
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. I'll give you an example to clarify more :
I have a plugin called lightGallery, the latter is only required when a user wants to open an image gallery. Logically, we can delay its load until all of the website more critical resources (like the pizza picture & important CSS) are downloaded.
I also have
Bootstrap
installed. ItsJavaScript
is only required when we need interactivity in our project, like for example : Opening a modal, Using a collapse or a carousel⦠So we can delay its load too.
1.1 - Lazy loading : LightGallery
In the following video, I'll explain in detail the process :
The code I used in the video :
// main.component.ts
let src = "https://jsdelivr.com"
window.onload = () => {
let script = document.createElement("script")
script.src = src
script.async = true
document.head.appendChild(script)
}
1.2 - Lazy loading Bootstrap
The same process goes with Bootstrap
, remember jsdelivr
? Search for 'bootstrap' and :
Copy the link and replace the old with the new one.
βΉοΈ Ps : Remember to remove any other imported Bootstrap JavaScript
1.3 - Bundle size
β Just like that, we eliminated (125.01 kB)
2 - Removing unused modules
My website is a single-page website
, even though, Angular routing
is installed. To fix this, all I need is to comment out AppRoutingModule
on my app.module.ts
Now, I need to replace <router-outlet></router-outlet>
with my parent component selector, which is app-main
2.1 - Bundle size
β We eliminated a total of (201.31 kB) from the initial build.
You can check the website after reducing the bundle size.
About lighthouse, the score has improved a little, but the website still has all the problems mentioned earlier. So, let's fix them.
Explaining Visual problem NΒ°1
Like I mentioned before, this website uses Bootstrap
, and styles.css
contains Bootstrap's
CSS. The reason of this problem is that Angular
started printing the website before styles.css
finished downloading, this means we had no stylesheet for Bootstrap
until styles.css
finished downloading.
To confirm this, we can try to block styles.css
from downloading at all and see if we have the same results.
βΉ And yea, the same result.
Solving Visual problem 1
To solve this problem, all my critical
CSS needs to be ready when Angular
start printing. 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
.
In my case :
Bootstrap's CSS
-
SiwperJS's CSS
.
But because I have buttons in the first page that are styled with some custom CSS, this CSS is also considered critical
.
Also, the animation
I'm using on my website are considered critical
too, the video below explains everything :
To resume, we have as Critical
:
Bootstrap CSS
,SwiperJS CSS
,Custom CSS
,Animations CSS
,
Now, let's get back to work.
First, I created a SCSS file named bootstrap.scss
, and I imported inside it only the Bootstrap component I need
βΉοΈ Note : you can import all of bootstrap
if you you want to, later I'll explain how we can remove unused CSS using PurgeCSS
.
And I did the same thing with SwiperJs
, Animations
, and my custom CSS
Next, I created a file named combined.scss
and imported all the SCSS files I just created
To clarify more, that's the list of files :
βΉοΈ Note : don't forget to remove the old imported CSS, ex : don't import Bootstrap on both styles.scss and combined.scss
.
After that, I jumped to angular.json, and under styles[]
:
{
"projects": {
"app": {
"architect": {
"build": {
"options": {
"styles": []
}
}
}
}
}
}
I added the following :
{
"input": "[YourPath]/combined.scss",
"inject": false,
"bundleName": "combined"
}
Then, I opened my index.html
and added the code below on the top of the <head>
tag
<!-- index.html -->
<link rel="preload" href="combined.css" as="style" />
<link rel="stylesheet" href="combined.css" />
What I've done here, is that the second a user visits the website, the first resource to be added to the download queue is combined.scss
, this means the browser will start downloading my website resources with combined.scss
on the top of the list, thus when Angular starts printing, the critical CSS are already loaded and ready to use.
Source : https://developer.mozilla.org
Bundle size
After building, I had this Lazy Chunk Files
section, with my combined.css
file there, in reality, I'm preloading, not lazy loading it.
You can also notice that the size of styles.css
dropped significantly
Now, thanks to PurgeCSS
, I'll try to reduce the size of combined.css
by removing unused CSS.
Installing PurgeCSS
On my command prompt :
# command prompt
npm i -D purgecss
After that, I created a file named purgecss.config.js
on the root of my project.
And inside it I have the following lines :
// purgecss.config.js
module.exports = {
content: ["./dist/**/index.html", "./dist/**/*.js"],
css: ["./dist/**/combined.css"],
output: "./dist/[FOLDER]/combined.css",
safelist: [/^swiper/],
}
βΉ Note : Remember to replace [FOLDER]
(within the output
property).
βΉ Note : you may notice that I have set safelist
to [/^swiper/]
, That's because I don't want PurgeCSS
to remove any SwiperJS
CSS. Because SwiperJS
will add some CSS classes after the page execution, thus PurgeCSS
can't know about them, and end up removing them.
Next, I opened package.json
, and edited build
from :
"build": "ng build"
To :
"build": "ng build && npm run purgecss "
Then I created a new script named purgecss
:
"purgecss": "purgecss -c purgecss.config.js",
To clarify, that's how package.json
should look like
β οΈ Note :
To build, use npm run build
instead of ng build
, So PurgeCSS
can kick in.
Result after solving Visual problem 1
Before and after using PurgeCSS
on combined.css
:
Now, let's take a look on the loading tree :
Like I explained before, combined.css
is now the first file on the download queue.
The downside of this method is that now we have two stylesheets (styles.css & combined.css), this means one more request to the server, and a couple of milliseconds wasted. Later I'll explain how I fixed this small issue.
Lighthouse
Even if lighthouse is telling me : 'your website is perfect', he is not 100% correct,
What about the Visual problem 2 & 3 & all the invisible problems?
βΉ Note : The website after this method
Explaining Visual problem 2 & 3
The cause of this problem, is the loading tree, or the order of the resources in the download queue.
Like you maybe already know, browsers have a limit of parallel requests.
Source : blog.bluetriangle.com
Because of that, I need to prioritize my resources. In other words, I need the font & the pizza picture downloaded before other low-priority resources.
Solving Visual problem 2 & 3
This problem is easy to fix, all I need is to preload (like we did earlier) the font, and the pizza picture.
We start with the font, but first, I need to know the name of the font i used on the first page (DayburyRegular
), after that, inside the <head>
of my index.html
I need to add :
<!-- index.html -->
<link
rel="preload"
href="[YourPath]/DayburyRegular.woff2"
as="font"
type="font/woff2"
crossorigin
/>
The same process goes for the pizza picture, so inside the <head>
of index.html
I also need to add :
<!-- index.html -->
<link rel="preload" href="[YourPath]/pizza.webp" as="image" />
It should look like this :
β οΈ Note :
Go to the CSS file containing your @font-face
:
If the path of your font is different from the path you put on your index.html, change it to the same as index.html, Even if they lead to the same file, they must be written the same.
Otherwise the browser will re-download the font (and i don't know why).
Now, I created a file named pre-fonts.scss
, and inside it, I transferred my font from its old SCSS file to this newly created one.
After that, I imported pre-fonts.scss
into combined.scss
, so its preloads with the other styles.
Result after solving Visual problem 2 & 3
Now, the font and pizza picture won't get late to the party again.
βΉ Note : The website after the fix
Explaining Visual problem 4
Like I explained on the video, when we lazy load resources that are not visible on the viewport, the other resources (that are visible) will load faster, because now we are downloading for example 10 images, instead of 100.
βΉοΈ TL;DR : We only need to load pictures that are actually needed and visible.
Solving Visual problem 4
The solution is to implement Lazy Loading
. There are many methods and techniques, but my personal choice is to go with lazysizes By Alexander Farkas.
But before, because we have all our critical CSS inside combined.css
, let's take a look at my styles.css
Poor file, looks so empty.
Since all the CSS inside this file are noncritical, why not just lazy load them with the same method I used when I Lazy loaded LightGallery.
So, inside angular.json
, under styles[]
I need to modify :
"styles": [
{
"src/styles.scss"
}
]
to
"styles": [
{
"input": "src/styles.scss",
"inject": false,
"bundleName": "styles"
}
]
Now, I need to to load styles.css
after all the resources finished downloading ( like I did when I Lazy loaded LightGallery)
So, inside main.component.ts
, under ngAfterContentInit()
and within window.onload
I added :
var link = document.createElement("link")
link.rel = "stylesheet"
link.type = "text/css"
link.href = "styles.css"
document.head.appendChild(link)
Now let's get back to the problem. I would be very pleased to explain to you how I lazy loaded my website, but this post is already long enough, plus, that's not the main subject for today. So I'll jump directly to the results, however, I'm planning on writing a detailed step by step guide, and link it right here.
Result after solving Visual problem 4
Lighthouse
Mission accomplished β
βΉοΈ Note : While fixing my visual problems, all the invisible problems are solved too :
Invisible problem 1 was fixed when we solved Visual problem 4, (when I lazy loaded the website with
lazysizes
)Invisible problem 2 was fixed when we solved Visual problem 2 & 3 (when I preloaded the font, and the pizza picture)
You can visit the website after this last step.
The Take
When your website starts printing, make sure that all of your critical CSS is ready.
If necessary, preload some of your resources (the main ones), for a better UX (like we did with the pizza picture & the font).
Always, like always lazy load your images, and if possible, lazy load your uncritical JS & CSS.
Try to install the minimum of third-party libraries, and uninstall the unused ones.
Always open your dev-console, and analyze and prioritize the order of your resources.
You may find mistakes related to my English, maybe I was wrong about some of the things I said, or the way I explained them. Your suggestions and advices are always more than welcome.
βΉοΈ Note : I tried to be beginner friendly as possible, which is why you find me a little repetitive and boring in some cases.
Top comments (5)
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.