Lazy Angular: Writing Scalable AngularJS Apps
There are two major issues I have faced in the past few years, when writing AngularJS applications, and I have seen numerous other teams fighting the same battles. Out of these experiences the “Lazy Angular” approach came to life. It gives us a project structure which works for both, large and small applications. And it enables us to keep a somewhat consistent load time as new features come to life and our app grows.
The whole setup is based on the angular-lazy Yeoman generator. There, you will also find a description of the whole toolchain that comes with it and what each library is used for.
The project structure follows a component based approach. To keep Angular apps – or client side applications in general – maintainable, we should split them up in self-contained components. This also allows us to reuse UI elements across projects easier. Other frameworks like React or Polymer already push this approach more prominently to developers and it has proven to be the most convenient so far. Angular 2 is heading in the same direction and the upcoming version 1.5, which is currently in beta, will also introduce the component method. It is an addition to existing helper methods like directive or service and follows the familiar builder pattern used to define Angular modules. It enforces best practices like using controllerAs instead of working directly on the $scope object. You can read more about it on the official AngularJS blog.
The generator differentiates between four component types:
- Application component: This type of component is used as an entry point for an AngularJS app. It contains our routes configuration and provides application wide settings. It is basically used to put together the puzzle pieces of our project. Usually, we will only have one application component per project. But there might be scenarios where we could have two apps which share a lot of components, e.g. a public and admin section. In this case we might want to put both apps in the same project since their code base overlaps heavily.
- State component: These components contain our application states. Behind them is UI Router, which is currently the de-facto standard router. So you can use all the functionality of it within state components. One could ask why we do not make the states part of the application component since they are very specific to it? We will explore the reasoning behind it later in this article.
- General component: These are our reusable UI building blocks. The template for those relies on Angular's component provider and gives us an easy way to create custom elements.
- Directive component: Since we cannot create custom attributes with Angular's component provider, the generator includes a template for attribute directives to cover that gap.
Loading only what is needed
By default, Angular expects all parts of an application to be present when the bootstrapping process starts. This is feasible for smaller projects with few dependencies. But as the project grows, so will its loading time. At some point in time, it will take too long to load all code upfront and the user experience will suffer. And that is where the lazy part of “Lazy Angular” comes into play.
We already have split our application into smaller components. Now we need to teach Angular to load those only when they are needed. Luckily, Oliver Combe has already done a great job in this area with ocLazyLoad. It wraps the core components in a layer which enables us to inject additional modules after the actual application was bootstrapped.
Next comes UI Router. To configure our states, we would need to load the code behind them with all its dependencies. But again, that long tail would lead us to a situation where we would load most of our app code upfront. To get around this, we need to split the declaration and implementation of our states. Lucky us, there is a neat little library called UI Router Extras, which allows us to do exactly that. It extends UI Router with a feature called “Future States”. These are states which can be late bound. Think of them as variables which you declare when the application starts, but you assign them a value at a later point in time. We can use a simple JSON file to declare our states.
The name and url properties are the same we use to configure our states in UI Router. The type tells UI Router Extras which loader to use to retrieve that state. If you need to load some states differently than others you can introduce new types and configure the future state provider accordingly. The loading mechanism for the states is part of the angular-lazy package which automatically gets installed when you generate a new application. Finally, the src property tells the loader where the state component can be loaded from.
In the above illustration, you see roughly how the lazy loading of states works. If the user clicks on a link, UI Router will first try to find the target in it’s list of states. If the state is found, it means that it is already loaded and can be displayed immediately. If UI Router cannot find it, it will trigger a $stateNotFound event. UI Router Extras picks this up and searches within it’s list of future states. If a matching future state has been configured, then it will be loaded and shown afterwards.
Now, no matter if our project has five or fifty states, we only have to load the application component and the first state, e.g. the login screen or the home page, to show the running app to the user. The other parts will be loaded as the user navigates through the screens. The user agent now only needs to retrieve the parts of the app which are relevant to the user and thus saves memory and cache space. When rolling out a new release, we only need to update the components which were adapted and let the browser serve the other parts from the cache where possible. Loading an entire app for each user is usually a waste, since they might not need all of the functionality.
Optimizing network performance
Soon you will realize, that you end up with a lot of small files when you use the angular-lazy generator. Each requires a network request to load. The overhead each request introduces lets the load time suffer. But better loading performance was the main reason why we chose this approach in the first place. So how can we rectify that again? Let us think about it.
We know that when we load our application component, we will not only need the Angular module itself, but also the connected templates, configurations, route declarations etc. In cases where we have static dependencies like this, we would ideally load all parts at once. The same is true for some of our 3rd-party libraries. At the time we load Angular we also need at least UI Router, UI Router Extras and ocLazyLoad, to set up everything on the client side. With angular-lazy-bundler we can do exactly this without writing an extensive configuration. Since it can rely on the project structure given by the generator, the bundler can handle components automatically. It even supports nested components. The only thing you have to define manually is, which libraries to combine. A minimal application component looks something like this:
| | constants.json
| | default-locale.js
| | error-handling.js
| | routes.json
| | routing.js
The bundler will then go ahead and combine all pieces into one application.js file. Under the hood, it uses SystemJS Builder to find all parts it needs to combine. With that in place, we can load the application component with only one request instead of nine.
I recently gave a workshop on the whole approach and created an example app to show how an application based on “Lazy Angular” could look like. If you run the app you will see that whenever possible components are loaded asynchronously.
In the above screenshot, you see that the Readme component is loaded way after the index state, which contains the first screen the user sees. Since the Readme component uses Bootstrap to show the file contents, it is also loaded lazily. This way we have to load 300 KB (30 KB compressed) less data to show the user the first screen. The repository state component is not loaded at all, since the user has not accessed it yet.
To reduce the number of network calls required to load the application, we are going to merge files which always belong together next. For that we run gulp bundle in the project folder.
This brings the number of network calls from 128 down to 69, of which most are made to load user avatars. If you only look at the application resources, we now only have 6 instead of 69 requests. An average 3G connection has a round-trip time of 100ms. This means that the bundling saves us up to 6 seconds of load time.
Having a tried and tested project structure and toolchain to start with, we have more time analysing and implementing our customers needs. It is definitively an opinionated stack and it is not the holy grail for all projects, but I am sure a lot of projects can benefit from it.
ti&m surfer Moritz Baumotte liebt es Neues zu lernen. Er hat einen enormen Wissensdurst und grosses Interesse an der Software-Architektur. Dank der ti&m academy kann er sich regelmässig weiterbilden. Der Kurs «Certified Professional for Software Architecture – Foundation Level» (ISAQB-FL) mit Gernot Starke hat ihm besonders gut gefallen.Mehr erfahren
So. Da haben wir jetzt Private Cloud, Public Cloud und Hybrid Cloud. Und jetzt kommt dann noch eine Multi-Cloud. Wozu? Was ist da überhaupt der Unterschied?Mehr erfahren
Using Kubernetes is simple, for example as a managed service such as Azure Kubernetes Service (AKS) and especially for first-day operations. In the long term, you want to gain visibility into the cluster and monitor certain events – this applies to non-managed service clusters, too.Mehr erfahren
Herr Walter entdeckt einen Teil der IT-Landschaft auf seiner Zugreise nach Bern.Mehr erfahren
Viele Banken haben erst begonnen, sich intensiver mit dem Thema Cloud auseinanderzusetzen. Noch gibt es aber Bedenken, vor allem wie sich die Cloud mit dem Bankgeheimnis vereinbaren lässt. Antworten dazu gibt Martin Hess, Leiter Digitalisierung und Wirtschaftspolitik sowie Direktionsmitglied der Schweizerischen Bankiervereinigung.Mehr erfahren
Beim diesjährigen shake the lake haben wir 450 Gäste bewirtet. Bei Aktivitäten auf, in und neben dem Zürichsee kam jeder auf seine Kosten. Hier ein kurzer Rückblick mit den schönsten Bildern einer langen Sommernacht.Mehr erfahren