If you are new to Aurelia or perhaps already building something with it, here are some tips and tricks I have learned over the last two years which have helped me write well-performing Aurelia applications.
Keep in mind that Aurelia is like any other framework or library and that it is very easy to write poor performing applications if you’re not careful. Never assume that a framework will stop you from writing bad code.
If vs Show Bindings
In Aurelia there are two attributes: if
and show
which are similar to one another, but have some fundamental differences. The if
binding will add or remove elements from the DOM, whereas the show
binding just applies a conditional display:none
to whatever element the attribute is used on.
Understandably, for some it is confusing. Do you use if
or do you use show
? My rule of thumb is defined by how often I am going to show or hide something in my application.
Am I going to show or hide an element in the page regularly or am I conditionally going to show or hide something sporadically or only just once?
The upside of the if
binding is when the bound value is false, the elements in the DOM will be removed (freeing up DOM space and bindings will be removed). The downside being Aurelia needs to set up a new controller instance and bindings (which isn’t that big of a deal).
I am a huge fan of a light DOM, so I use if.bind
extensively because a large DOM is a lot slower than the time it takes for Aurelia to setup controller instances and bindings. Both have a purpose, it is important to use what works for you.
I do find if.bind
works well for me in most cases, no point having elements around if they’re not being used (in my opinion).
Avoid dirty-checking at all costs
If you’re not sure what dirty-checking is, essentially it’s a timer with a 120-millisecond timeout value that repeatedly loops and checks for changes. You definitely do not want dirty-checking in your application (unless you absolutely need it for some edge case).
If you define a getter in your view-model which accesses one or more values in your class and you do not supply the computedFrom
decorator, it will be dirty-checked.
An example of dirty-checking can be seen below:
export class MyClass {
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
<template>
<h1>${fullName}</h1>
</template>
Internally, Aurelia is going to dirty-check this using a setInterval
to constantly poll if the value changes, this is bad. Fortunately, using computedFrom
we can make this an optimised getter which only reacts when one or more dependencies change.
The below example does not use a timer to poll for changes, it makes the getter reactive.
import {computedFrom} from 'aurelia-framework';
export class MyClass {
@computedFrom('firstName', 'lastName')
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
<template>
<h1>${fullName}</h1>
</template>
A note on computedFrom: the this
part is implied, so when supply one or more computed values to computedFrom
you are automatically referencing the current class and do not need to supply this
.
A note on computed arrays
the computedFrom
decorator is great for telling Aurelia about your getter dependencies, but it doesn’t work for all values. One such situation is arrays, an array cannot be directly supplied as a getter computed value because it won’t update.
If you’re looking to force a getter to update when an array changes, computed based on its length. When the size of the array changes (items are added or removed) the getter will fire.
import {computedFrom} from 'aurelia-framework';
export class MyClass {
@computedFrom('arr.length')
get firstValue() {
return `${this.arr[0]}`;
}
}
<template>
<h1>${firstValue}</h1>
</template>
Think big by thinking small
If you come from an MVC background, then you’ve heard the expression, “fat model, skinny controller” and other countless expressions around MVC architecture which have sort-of been carried over into the front-end development jungle.
I prefer fat nothing in my Aurelia applications (or web applications full-stop). If your view-models and/or views are massive, then you have a code architecture problem which is easily solved by reusable components.
A large view-model or view is eventually going to be your undoing (besides the annoying Git conflicts you will encounter working in a team on large files). Large files which do too much are counterproductive and unnecessarily complicated.
If you want to ensure your application is easy to build upon and make it easier for the browser, build tools and test runners to work with your code: break things up into reusable components.
I highly recommend approaching the architecture of your Aurelia applications using the Single responsibility principle.
Use view-cache
This isn’t documented very well, but Aurelia provides a view-cache which can allow you to cache heavy views. This means if you have quite a complex view and you navigate away, but come back to it later Aurelia will use the view-cache and update all of the appropriate bindings.
The benefits are obvious: the whole view doesn’t need to be recreated when you navigate back to it. Just throw view-cache
onto the main opening <template view-cache="*">
tag, which also accepts a cache size value.
If you supply an asterisk *
to view-cache
it will cache all instances or if you supply a numeric value, cache the number supplied.
Use compose when nothing else fits
Aurelia provides a <compose>
custom element which allows you to dynamically compose from within your application. You can dynamically compose view-models, views and pass in data as well.
As great as <compose>
is, I try and avoid it wherever I possibly can. It’s not that I think <compose>
is bad, it’s the fact that <compose>
is an abstraction and where possibly, I try and avoid abstractions in my applications.
I have experienced instances where <compose>
has made debugging more difficult than straight-up view-models and components.
Assess your needs and if <compose>
is the only option, then by all means use it.
Use Binding Behaviors
Out-of-the-box, Aurelia ships with some great binding behaviours which allow you to make bindings in your applications more efficient and performant.
A binding behaviour in your view templates is denoted by an ampersand “&” and much like value converters, they can be chained and have parameters which accept values.
throttle
A rate-limiting throttling behaviour which allows you to throttle the rate in which the view-model is notified of a change in a two-way binding situation. By default the throttle value is 200ms.
debounce
Very similar to the throttle
binding behaviour, the debounce
behaviour will prevent a binding from being updated until the debounce interval completes after no changes have been made.
updateTrigger
Allows you to specify which input events signal to Aurelia’s binding system that a change has been made. By default, the events that trigger a change are change
and input
.
signal
Allows you to tell Aurelia’s binding system to refresh a binding. It works like an event listener on a binding and when you fire an update signal, it refreshes the binding which works similarly to how a first-page load does.
oneTime
Use the oneTime
binding behaviour as much as possible. It allows you to tell Aurelia’s binding system to display a binding value and then forget it. A one-time binding will not update if the value changes, which can lead to great application performance.
If you would like to see examples of binding behaviours and how to work with them, Aurelia core team member has a great write-up on working with binding behaviours here which I recommend you also read.
Batch DOM changes using the Task Queue
The Aurelia Task Queue is a powerful module that allows you to sort of hook into the browsers event loop which allows you to perform DOM actions in an efficient way.
While slow Javascript code can definitely affect performance, it is a known fact that the DOM is the bottleneck of any web application, which is why libraries like React and Vue leverage a virtual DOM. Working with the DOM inefficiently can result in poor browser performance.
By queuing up a microtask, we can batch changes in the page resulting in less reflow and repaints.
import {TaskQueue} from 'aurelia-task-queue';
export class MyViewModel {
static inject = [TaskQueue];
constructor(taskQueue) {
this.taskQueue = taskQueue;
}
attached() {
this.makeSomeChanges();
}
makeSomeChanges() {
this.taskQueue.queueMicroTask(() => {
const header = document.getElementById('header');
// Dynamically set the width and height of an element with ID "header"
// We are batching DOM changes, so we only get on repaint/reflow
header.style.height = '250px';
header.style.width = '500px';
});
}
}
Be smart about event handling
In Aurelia, there are two ways to create event listeners. You can create an event listener the Javascript way using addEventListener
or you can also bind to events from within your views with one such example being click.delegate="myCallback()"
.
Where possible, you should use Aurelia’s templating for binding to events. The biggest advantage being Aurelia cleans up after itself and removes events when they’re no longer being used.
This is not to say that addEventListener
is completely redundant, but for eventing around elements in your page, you can and should use Aurelia’s provided way of working with native events.
You’ll have the piece of mind that events are properly being unbound when views are being destroyed, it’s like programming languages that automatically manage memory for you like PHP.
Don’t abuse the Event Aggregator
Aurelia comes with a great Event Aggregator module which allows you to publish and subscribe to events from within your application. The routing module also uses the Event Aggregator, firing off various events at different stages of the routing process.
As great as the Event Aggregator is, I have seen it abused quite a lot. I have done a bit of Aurelia consulting and in most instances, the Event Aggregator is being used for things it shouldn’t be used for.
The biggest issue you will encounter with the Event Aggregator is that it is hard to debug. This rings true for most custom eventing, not just Aurelia’s module.
So, when are you supposed to use the Event Aggregator then?
Do’s
- Use the Event Aggregator to notify parts of your application about specific actions; user added, recorded deleted, a user logged in, a user logged out, etc.
- Use the Event Aggregator to react to specific actions; a modal opening or closing, a user is deleted or if the user logs out clearing out a token in localStorage, etc.
Do nots
- Don’t use the Event Aggregator for passing data between routes
- Don’t use the Event Aggregator for passing data between components
- Never treat the Event Aggregator as a crucial dependency (creating components that rely on specific events and values)
Passing data between routes
A common scenario I have witnessed in the community is wanting to route from Route A to route B and pass some data to the route you are navigating too.
I’ve seen developers pass data using the Event Aggregator (see above, don’t do that), use localStorage
(also, don’t do that) and the even crazier: storing it on the route object itself (seriously, don’t do that).
The way that I always recommend passing data between routes and components is by using a singleton class. If you come from an Angular background, then you might know this as a service class.
Assuming you have two routes: Route A and Route B, and in Route A the user can enter their name and then be navigated to RouteB which will display their name.
I would achieve this in the following way:
import {AppService} from './app-service';
export class RouteA {
static inject = [AppService];
constructor(appService) {
this.appService = appService;
}
}
<template>
<h1>Welcome: Route A</h1>
<label>Please enter your name: <input type="text" value.bind="appService.name"></label>
</template>
import {AppService} from './app-service';
export class RouteB {
static inject = [AppService];
constructor(appService) {
this.appService = appService;
}
}
<template>
<h1>Hello, ${appService.name}</h1>
Welcome to RouteB. The name you provided on the previous page is displayed above.
</template>
// Assuming this file is saved as app-service.js alongside the other route view-models
export class AppService {
constructor() {
this.name = '';
}
}
Or better still, use a state management solution if your application state is complex and requires a much more manageable solution. The Aurelia Store is my favourite and recommended state management solution for Aurelia applications.
This post is pure gold. Thank you so much for the write-up!
Really useful information! Thanks
Hi Dwayne,
Sean Hunter advocates using Event Aggregator for intercomponent communication in his book “Aurelia in Action”, so i am a little confused now.
If i don’t use the Event Aggregator for passing data between components, that are far apart in the component tree, what should i do then?
Use custom events and let them travel up (and down) through n-components?
Use aurelia-store?
Thanks in advance
Best,
Oliver
It’s all personal preference. I find events are harder to debug, they are somewhat of a BlackBox and you can’t really step through them. I use Aurelia Store a lot these days, but in instances where I do not, I use a simple singleton service class and methods for updating and retrieving data. You simply inject your singleton into whatever component needs the data and voila. The issue with singletons is Javascript variables are assigned by reference, so the singleton approach can fall flat because the source value can be changed without any kind of audit trail.
When you think about it, the store plugin is a pub/sub eventing system, the difference being it has inbuilt support for debugging and works on the use of functions and state changing over time. The Event Aggregator would work, but it’s nothing more than sending a one-way message into the ether and hoping someone listens. I tend to use the Event Aggregator for events in my app like:
– Language dropdown changed
– Use clicked delete in the header (or some other button)
More of a notification system that something has happened, rather than what the specifics are data-wise. The uses of the Aggregator by Aurelia itself (notably the router) are just to tell you things are happening, lifecycle events like the router is navigating away or there was an error.
If you have a lot of global state in your application, I would definitely consider using Aurelia Store and also look into the Aurelia Async Binding Behaviour (this will be in the core in Aurelia vNext).
Hello Dwayne,
thank you very much for taking time and writing such a torough answer.
It really helped me put things into perspective. As a matter of fact, i think
you could use some of it for the official documentation.
Speaking for me i would love to introduce a store / the store plugin and some RxJS(♥).
An Async Binding Behavior would be great!
Till then, there´s ZEWA666´s project:
https://github.com/zewa666/aurelia-async-binding
Thanks again, Best regards