Angular: Multiple Themes Without Killing Bundle Size (With Material or Not)
Keep users happy with a choice of themes
This guide is also interesting if you don’t use Angular Material, it can easily be applied to other UI libraries or your own library.
Angular Material makes our life simple when we need to theme our app.
You can add as many themes as you want and it can be interesting for your users to choose between them easily, with one click, to provide a site well-suited to your user.
But… You have some pain points to resolve:
- You have to distinct each theme by providing a class in your main CSS file.
- You have to create a logic to manage these classes inside your app when a user selects a theme.
- Every theme will include the mix-in and you’re increasing the bundle size. It’s a lot to download for your users.
- You will have a lot of unused CSS properties in your file.
Let’s say we have an app containing two themes: the default and the dark theme.
We can create a
_theme-base.scss to be DRY:
And our two themes:
And in the end, we create the classes to apply the themes:
With this configuration, our bundle size is 184KB.
(I used the option
angular.json to get the CSS file instead of the JS file, to be consistent between prod and dev. You will understand why later in this article)
I recently worked on a project where there were multiple themes (more than six). The bundle size was heavy: 1.56MB.
After optimization, I succeeded in reducing this size to ~150KB per theme, thus 90% of the CSS was unused by the user and it has to download it!
Managing Multiple Themes
Here is a guide I’ve created to manage multiple themes without killing the user experience with a heavy style-bundle size.
1. Create a separate folder to store your theme files
As you’ll have all your themes stored in one place, it will be easy to locate them, like this:
2. Modify angular.json to extract CSS themes into files
When you develop, Angular converts your styles into JS files. It is faster during development.
Just for the tests, you can disable this behavior by adding an option in your
angular.json. This is activated by default in your production’s options.
Now, we have to create a separate CSS file per theme. To do this, you have to modify the
angular.json once again by specifying the files you want to create.
input: takes the path of where your file is located.
lazy: if true, indicates to Angular that we don’t want to add the file to the head of our HTML automatically.
bundleName: is just the output name.
At this point, if you modify the property
lazy: true for one of your themes, you will directly see the result in the browser. But, the real interest is to load it dynamically.
We can do this, for this example, with a service.
3. Create a service to manage themes
This service is necessary to do two things:
- Add the theme’s CSS file in the HTML’s head.
- Switch between themes and dark mode.
I will focus on the first task, and just show how I did it with the second one.
Take a look at this implementation and read the explanations below:
When you’re using a service, you won’t be able to get the renderer.
- Line 26 — Sometimes, Angular had this kind of problem, that’s why they created RendererFactory2. With this, we can create a renderer and use it inside a service. You also inject the document, to get access to the
Don’t worry about the parameters set to
null. The first parameter is
hostElement with type
any and the second is
type with type
RendererType2|null . So, there is no problem to set them to
- Line 45 — The tricky part is the
loadCssmethod. Using our renderer, we create the appropriate link tag which will be added inside the head as a child element (line 52).
- Line 51 — To avoid having a page without style, we wait for the end of
onloadby giving it the resolve of our promise. So, the promise ends when the CSS file is loaded.
The rest of the code assures that we only use one theme at a time, to improve the critical path rendering of our page.
4. Apply the service to your component and let the magic happen
It’s finally time to use our
ThemeService and see how incredible it can be!
Here is the HTML to test our theme:
And its component:
And you can see the result directly in your browser.
But the real interest is in your bundle size. Instead of the 184KB in the beginning, you should have:
Suggestions to Optimize
This is an example of the implementation but it is not the best way.
In a real-world app, you have to manage the cache so the user doesn’t download the file again and again (imagine if your user changes their mind every day?). This can be achieved by using a service worker.
Also, the theme initialization can be implemented directly in the
APP_INITIALIZER. This can be interesting and avoids Angular Material shouting for:
Normal, because we delayed the load of the Angular Material theme.
Have a nice day and happy coding!