Angular Performances Part 1 - First load
We have just finished a new chapter of our ebook about performances, and we thought we could share it with you in a series of blog posts. It took us a long time, but we wanted to write something more complete than what you can usually find. There are a lot of tips to make Angular faster (whatever faster means for you, we’ll come back to this in a minute), but you usually don’t have the other side of the story: what are the traps of these optimizations, are they what you are looking for, and should you really use them.
This is the first part of this series, and this blog post is about the first load of an Angular application. In future posts, we’ll talk about how to make reloading faster, then about how to profile your running application, and how to improve runtime performances. If you are the lucky owner of our ebook, you can already check the other parts if you download the last ebook release.
Warning: be careful with premature optimization. Always measure before and after. Beware of the benchmarks you find on the internets: it’s pretty easy to make them say what the authors want.
Performances can mean a lot of things: speed, CPU usage (battery consumption), memory pressure… Everything is not important for everybody: you have different needs if you are programming for a mobile website, an e-commerce platform, or a classic CRUD application.
Performances can also be split in different categories, that, once more, won’t all matter to you: first load, reload, and runtime performances.
First load is when you open an application for the first time. Reload is when you come back to that application. Runtime performances is what happens when the application is running. Some of the following advices are very generic, and could be applied to any framework. We wrote them because we think it’s worth knowing. And because when you talk about performances, the framework is sometimes the bottleneck, but really (really) often not.
When you load a modern Web application in your browser, a few things happen. First, the
index.html is loaded and parsed by the browser. Then the JS scripts and other assets referenced are fetched. When one of the assets is received, the browser parses it, and executes it if it is a JS file.
So the first tip is very obvious: be careful with your assets sizes!
The assets loading phase depends on how many assets you want to load. A lot will be slow. Big ones will be slow. Especially if the network is not that good, which happens more often than you think: you might test your application on a fiber optic connection, but some of your actual users might be in the middle of nowhere, using slow 3G. Here is what you can do.
Bundle your application
Webpack (or the other tool you use) starts from the entry point of your application (the main.ts file that the CLI generated for you, and that you probably never touched), and then resolves all the imports tree, and outputs the bundle. This is cool because the bundle will only contains the files from your codebase and your third party libraries that have been imported. The rest is not embedded. So even if you have a dependency in your package.json that you don’t use anymore (so you don’t import it anymore), it will not end up in the bundle.
It’s even a bit smarter than that. If you have a file
models exporting two classes, let’s say
RaceModel, and then only import
PonyModel in the rest of the application, but never
RaceModel, then Webpack only puts
PonyModel in the final bundle, and drops
Pony with two methods
run, but you only use
run, the code of the
eat method will be in the final bundle. So it’s not perfect, but it does a good job.
A few techniques can be used in Angular specifically to have a better tree-shaking. First, don’t import modules that you don’t use. Sometimes you give a try to a library offering a wonderful component, and you add the NgModule of this library to the imports of your NgModule. Then you don’t use it anymore, but maybe forget about the module import, and don’t remove it… Bad news: this module and the third party library will be in the final bundle (for now, maybe it will be better in the future). So only import and use what you really need.
Another trick is to use
providedIn for your services. If you declare a service in the providers of your NgModule, it will always end up in the bundle whether you actually use it or not, simply because it’s imported and referenced in the module. Whereas if you don’t register in the providers of your NgModule, but use
providedIn: 'root' instead, then if you never use this service, it will not end up in the bundle.
Minification and dead code elimination
When your bundle has been built, the code is usually minified and dead code will be eliminated. That means all variables, methods names, class names… are renamed to use a one or two characters name through the entire codebase. This is a bit scary and sounds like it could break things, but UglifyJS has been doing a great job for years now. UglifyJS will also eliminate dead code that it can find. It does its best, and I was saying above, the CLI team built a tool that prepares the code with special comments on unneeded code, so UglifyJS can remove it safely.
While the above sections were about JS specifically, your application also contains other assets, like styles, images, fonts… You should have the same concerns about them, and do your best to keep them at a reasonable size. Applying all kind of crazy techniques to optimize your JS bundle sizes, but loading several MBs of images wouldn’t have a big impact on your page loading time and your bandwidth! As this is not really the scope of this post, I won’t dig into this topic, but let me point out a great online resource by Addy Osmani about image optimization: Essential Image Optimization.
All the modern browsers accept a compressed version of an asset when they ask the server for it. That means you can serve a compressed version to your users, and the browser will unzip it before parsing it. This is a must do because it will save you tons of bandwidth and loading time!
Every server on the market has an option to activate the compression of assets. Generally the first user to request an asset will pay the cost of the compression on the fly, and then the following ones will receive the compressed asset directly.
The most common compression algorithm used is GZIP, but some others like Brotli are also popular.
The good news is Angular (its router, and its module system, in particular) makes this task relatively easy to achieve. The other good news is that the CLI knows how to read your router configuration to build several bundles automatically. You can read our chapter about the router if you want to learn more.
Lazy-loading can vastly improve the loading time, as you can make the first bundle really small, with only what’s needed to display the home page, and let Angular load the rest on demand when your user navigates to another part. You can also use prefetching strategies to tell Angular to start loading the other bundles when it’s idle.
Note that lazy-loading adds complexity to your application (and a few traps with dependency injection), so I would advise to go this way only if it really makes sense.
Ahead of Time compilation
This is not optimal in production for two reasons mainly:
- every user pays the cost of this template compilation on every reload;
- the Angular compiler must be shipped to your users (and it’s big).
Server side rendering
I’d like to start by saying that this technique is for 0.0001% of you. Server side rendering (or universal rendering) is the technique that consists of pre-rendering the application on the server before serving it to the users. With this, when a user asks for
/dashboard, she will receive a pre-rendered version of the dashboard, instead of receiving
index.html and then let the router do its job after Angular has finished to start.
It can lead to vast improvements in perceived startup time. Angular offers a package
@angular/universal that allows to run the application not in a browser but on a server (usually a NodeJS instance). You can then pre-render the pages and serve them to your users. The page will display very fast and then Angular will start its job and run as usual.
The bad news is that it’s not as easy as adding the
@angular/universal package. You application needs to follow some best practices (no direct DOM manipulation for example, as the server won’t have a real DOM to manipulate). Then you need to setup your server and think about the strategy you want to adopt. Do you want to pre-render all pages or just a few? Do you want to pre-render the whole page, with the data fetching and authorization check it will need, or just some critical parts of the page? Do you want to pre-render them on build, or to pre-render them on demand and cache them? Do you want to do this for all the possible profiles and languages or just some? All these questions depends on the type of application you are building, and the effort can vary greatly depending on your goal.
So, again, I would advise you to use server side rendering only if it is critical for your application, and not based on the hype…
If you enjoyed this blog post, you may want to dig deeper with our ebook, and/or with a complete exercise that we added in our online training. The exercise takes an application and walks you through what we would do to optimize it, measuring the benefits of each steps, showing you how to avoid the common traps, how to test the optimized application, etc. Check it out if you want to learn more!
See you soon for part 2.