Lazy-loading CSS in your AngularJS app
Download and inject stylesheets on demand using $route.resolve
Follow me on Twitter for latest updates @gerardsans.
Meet the monolithic stylesheet
It’s common practice to use one big CSS file for the whole application. This file is usually the result of various build steps: compilation, concatenation and minification. Tools such as Gulp or Grunt are great at this.
Relying on one big stylesheet is all good and fine for small projects but what happens when the website keeps growing and more rules are added to it? As you keep adding new sections and components, it gets bigger and bigger. One day it hits your desk… The first visit to your app is taking few seconds to load… You dig a little into it and find that there is a big monolithic CSS sitting in your browser.
What options are there for you now? Well, you clearly have to split your CSS into separate files. A sensible option would be to make a stylesheet for the different sections of your application.
This is just one of the reasons why you would want to cut down on CSS. Here’s some other scenarios why you should lazy-load your stylesheets:
- Mobile performance. A slim CSS bundle will load faster and render quicker (critical rendering path). This is especially true for mobile devices. After loading just the minimal styling, you can progressively load more CSS as needed.
- CSS reflow. You are already loading CSS files using the link element but are facing CSS reflow issues you want to avoid. Remember the browser has to download the content after adding the link element and even after that, the new stylesheets take few more ms to be available to the DOM.
In Angular your routes are a good place to start when chopping up your application into sections. Each route already loads its corresponding template and attaches the controller. It would make sense to also load stylesheets this way, but unfortunately that’s not supported. You could inject CSS into your template but then you will be missing out on browser caching and making your templates bigger.
Splitting CSS files using $route.resolve
In order to inject our CSS file while navigating to a new route we will use $route.resolve. We are going to use resolve in a way that navigation will only happen after we have injected our CSS. See $routeProvider.when() for more details.
We moved the actual code to a factory Service called injectCSS that contains a function set(id, url) returning a promise. This is the actual implementation of the service.
This code takes ideas from different sources and adds some personal choices in. See VIISON/RequireCSS and stackoverflow.
In order to inject our CSS we:
- inject a link tag into the head element
- hook to link.onload event (fires only in some browsers)
- check document.styleSheets for changes
In case the CSS fails to load, Eg. network failure, it will keep on going after the browser adds an entry on styleSheets.
While working on the final solution I found out about a couple of things I didn’t know before:
- dynamically injecting a CSS files is not as easy as it seems. There are lots of quirks along the way. This solution only covers the surface and I am just getting to see why the Angular team left it out.
- $http apparently doesn’t use browsers cache opposed to the href on the link element. $http will always make an initial request even after being cached on the browser using the link tag.
- It makes no difference having called $http with the same CSS file. The link tag will ignore it and take the same time to parse it and paint it on screen.
Use this code with precaution as it has not been tested on all browsers.
Thanks Gert Hengevelt for kindly reviewing this post and publishing it at Opinionated AngularJS.