Increasing Performance with Efficient DOM Writes in Ionic
I've touched on this point many times in the past, so I will try to keep this brief. Ionic makes it super easy for people to build mobile applications, but that low barrier to entry (which is a good thing in general) means that it also makes it super easy to build bad applications. You will need in-depth knowledge of web performance concepts and how Ionic works if you want top notch performance for more complex applications.
This is the case with any framework or even if you're building your application directly with native code, it's never easy to build a complex application that performs well without a good level of knowledge of the tools you are using.
For more on this particular topic, you may want to read Ionic Framework Is Fast (But Your Code Might Not Be).
In this article, I am going to show you how to make efficient writes to the DOM (Document Object Model) using a service that Ionic provides. One of the biggest limiting factors for performance on the web is modifying the DOM, which means doing things like inserting new elements (or "nodes") into the page or updating existing ones.
In order to allow for features like smooth list scrolling, the Ionic team have given careful attention to how and when updates are made to the DOM. However, if we come in and start forcing DOM updates whenever we want we can ruin the general flow of things (imagine driving your car on the opposite side of the freeway and messing up the nice flow of traffic).
We will discuss a little theory first, and then get into how you can make your DOM updates more efficient.
Outline
Layout Thrashing
Before I get into how to efficiently make updates to the DOM in Ionic, I want to cover a little about the why, and to do this I am going to introduce the concept of Layout Thrashing.
After a web page has initially loaded and rendered, obviously we can continue to make updates to the page after everything has loaded, and the page is going to need to update itself to reflect those changes. When making these changes, we need to be aware of repaints and reflows (or layouts).
A repaint is caused by changes that affect the visibility of elements, but not the layout. You might make a change to the colour of an element, and in response to this, the browser then needs to make that change and display it on the screen.
Repaints will cause a performance hit to accomodate this change, but a reflow (or layout) will cause an even bigger performance hit. A reflow is caused by doing things like changing the size or position of an element, adding a new element, or removing an element. You could make a change that affects the position of everything else on the page, so the browser needs to recalculate where everything should belong - this is an expensive operation in terms of performance.
During a repaint or a reflow, the browser will become blocked, and no other tasks can be performed whilst the repaint or reflow is happening. This happens really quickly, so generally, it is not going to be noticeable.
Layout thrashing can cause issues, though. Even though an individual repaint or a reflow is performed quite quickly, if we are hammering our application by constantly triggering a lot of repaints and reflows, there is going to be a noticeable performance impact. This can especially become noticeable with making DOM updates in response to events like scrolling, where many events are fired off constantly rather than just a single event with a button click for example.
If you're interested in exploring these concepts more, you might be interested in checking out the Creating High Performance Ionic Applications module.
Efficient DOM Writes
Repaints and reflows are unavoidable (if we want the browser to update the page), but they can be optimised. One big improvement that can be made is to make DOM updates in batches, rather than just updating the DOM whenever we want. If you want to make 5
updates to the DOM, rather than doing one at a time and causing repaints and reflows for all of them, you could do all 5
at once and only have to deal with one reflow or repaint.
Fortunately, we don't need to worry too much about this because Ionic has done a lot of the work for us (although, you should still attempt to minimise things that are going to cause repaints and reflows in your application). Ionic has a function that is provided through the DomController that can schedule DOM updates for us in a way that is both efficient, and won't get in the way of everything that Ionic is doing behind the scenes.
Using DomController
If you want to write to the DOM efficiently, you can simply import the DomController and use the write
method from there. As I mentioned, this is especially important if you are making frequent updates to the DOM (e.g. in response to scroll events) but it is also a good practice to just use DomController
whenever you are writing to the DOM.
The DomController
can be imported from @ionic/angular
and injected through the constructor:
import { DomController } from '@ionic/angular';
constructor(private domCtrl: DomController) {
}
and writes to the DOM can be triggered like this:
this.domCtrl.write(() => {
// DOM writes go here
});
Let's put this into context.
The following example is from the Advanced Angular Components & Directives module, and in this particular lesson we walk through creating a directive that allows you to easily add a parallax header to the content area. The general idea is that the header image "scrolls" at a different speed to the rest of the content, which creates the illusion of depth.
There are two points in time where we are writing to the DOM:
- When we inject the header image intially
- When we update the header image in response to scroll events
We can use the DomController
in both of these circumstances, but it is far more important that we do in the second case. In the second case, many DOM writes will be triggered whenever the user scrolls, whereas in the first case it just happens once.
Implementing the DomController
for the second case would look something like this (where onContentScroll
is triggered whenever the content area is scrolled):
@HostListener('ionScroll', ['$event'])
onContentScroll(ev) {
let translateAmt: number;
let scaleAmt: number;
const scrollTop = ev.detail.scrollTop;
// Already scrolled past the point at which the header image is visible
if (scrollTop > this.parallaxHeight) {
return;
}
if (scrollTop >= 0) {
translateAmt = -(scrollTop / 2);
scaleAmt = 1;
} else {
translateAmt = 0;
scaleAmt = -scrollTop / this.headerHeight + 1;
}
this.domCtrl.write(() => {
this.renderer.setStyle(
this.header,
'transform',
`translateY(${translateAmt}px) scale(${scaleAmt})`
);
this.renderer.setStyle(
this.mainContent,
'transform',
`translateY(${-scrollTop}px)`
);
});
}
You can see in the code above that the two modifications we are making to elements in the DOM (the header and the content area) are contained within the write
method of the DomController
. This will allow the DomController
to schedule those updates in an efficient manner, rather than forcing them immediately.
Summary
With quite a simple change to an application, we can drastically improve performance by making sure we are making efficient updates to the DOM. Typically in the past, this has been done by using requestAnimationFrame
but Ionic simplifies this with the DomController
(and it also allows for better coordination with the internals of Ionic components).
Hopefully, this also illustrates my point about how easy it is to make a bad application with Ionic: how is a beginner Ionic/Angular developer supposed to know that making DOM updates in response to scrolling is going to negatively impact performance? It's not too unreasonable to assume that you simply listen for some event like scrolling (which will rapidly fire off events), and then use that event to update something in the DOM – but if these updates aren't run through Ionic's DomController
, it would probably have a terrible impact on performance.
Often, the natural conclusion that follows from that situation is "well, Ionic is just slow" not "I'm making inefficient updates to the DOM which is causing lag in my application".
It's a case of "you don't know what you don't know", and if you want to create complex applications with Ionic that perform well, these are the sorts of concepts you will need to learn. Despite the speed of which Ionic allows you to begin making mobile applications, even with little experience, these concepts naturally take a long time to learn and master.