Increasing Performance with Efficient DOM Writes in Ionic 2



·

I’ve touched on this point many times in the past, so I will only make it briefly now. Ionic and HTML5 mobile applications in general, make it super easy for people to build mobile applications, but it also makes it super easy to build bad applications. It’s not always easy to build good applications with Ionic – anything more than a simple application will require more intimate knowledge of web performance and how Ionic works if you want top notch performance. This is the case with any framework or if you’re building your application directly with native code, it’s never easy to build a complex application that performs well (Ionic just has a lower barrier to entry).

In the case of Ionic and other HTML5 mobile applications, I think it’s mostly a matter of “you don’t know what you don’t know” so people may end up building applications in a way that seems correct and that works, but really they are making critical mistakes that are going to impact performance.

If you want to read more of my thoughts on why the ease of which Ionic applications can be made is both a good thing and a bad thing, you might be interested in reading another recent article I wrote: Is Ionic the WordPress of the Mobile App World?

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 2 provides. One of the biggest limiting factors for performance on the web is modifying the DOM, that is doing things like inserting new elements into the page or updating existing ones.

In order to allow for features like smooth and native-like 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 everything (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.

Layout Thrashing

Before I get into how to efficiently make updates to the DOM in Ionic 2, 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.

A repaint is caused by changes that affect the visibility of elements, but not the layout. You might make a change to the colour or opacity of an element, and in response to this, the browser then needs to check if other elements should now be visible (if there was an element that was behind the element that you just reduced the opacity of for example).

Repaints will cause a performance hit, but a reflow 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 on the page, so the browser needs to recalculate where everything should belong.

During a repaint or a reflow, nothing else 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 a repaint or a reflow is performed quite quickly, if we are hammering our application by constantly triggering tons of repaints and reflows, there is going to be a noticeable performance impact. This can especially become noticeable with making DOM updates with events like scrolling, where many events are fired off constantly rather than just a single event with a button click for example.

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 minmise 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.

DOM Writes for Scrolling

As I mentioned, it is especially important to ensure you are efficiently updating the DOM when doing so in response to a scroll event since the scroll event is fired so rapidly. Ionic provides direct access to the write method through the scroll event:

onContentScroll(ev){
 
    ev.domWrite(() => {
        this.updateParallaxHeader(ev);
    });
 
}

The function above is a handler for the ionScroll event that Ionic’s Content component (i.e. your <ion-content> in the template) emits. I pass the $event through to this function, and then I am able to directly access the domWrite method through the scroll event. By performing my DOM update inside of this handler, I can ensure that Ionic will handle the update for me and do it in the most efficient way possible.

Click here to read the full tutorial.

Other DOM Writes

If you want to write to the DOM, but you are not doing so in response to a scroll event, you can simply import the DomController and use the write method from there.

The following example is from a recent tutorial I created where I built a sliding drawer component that could be pulled up over the top of the main content. To do this, I was responding to the pan event, not the scroll event, so I had to access the write method through the DOM controller:

import { Component, Input, ElementRef, Renderer } from '@angular/core';
import { Platform, DomController } from 'ionic-angular';

@Component({
  selector: 'content-drawer',
  templateUrl: 'content-drawer.html'
})
export class ContentDrawer {

    // ...snip

    handlePan(ev){

      let newTop = ev.center.y;

      let bounceToBottom = false;
      let bounceToTop = false;

      if(this.bounceBack && ev.isFinal){

        let topDiff = newTop - this.thresholdTop;
        let bottomDiff = (this.platform.height() - this.thresholdBottom) - newTop;      

        topDiff >= bottomDiff ? bounceToBottom = true : bounceToTop = true;

      }

      if((newTop < this.thresholdTop && ev.additionalEvent === "panup") || bounceToTop){

        this.domCtrl.write(() => {
          this.renderer.setElementStyle(this.element.nativeElement, 'transition', 'top 0.5s');
          this.renderer.setElementStyle(this.element.nativeElement, 'top', '0px');
        });

      } else if(((this.platform.height() - newTop) < this.thresholdBottom && ev.additionalEvent === "pandown") || bounceToBottom){

        this.domCtrl.write(() => {
          this.renderer.setElementStyle(this.element.nativeElement, 'transition', 'top 0.5s');
          this.renderer.setElementStyle(this.element.nativeElement, 'top', this.platform.height() - this.handleHeight + 'px');
        });

      } else {

        this.renderer.setElementStyle(this.element.nativeElement, 'transition', 'none');

        if(newTop > 0 && newTop < (this.platform.height() - this.handleHeight)) {

          if(ev.additionalEvent === "panup" || ev.additionalEvent === "pandown"){

            this.domCtrl.write(() => {
              this.renderer.setElementStyle(this.element.nativeElement, 'top', newTop + 'px');
            });

          }

        }

      }

    }

}

The pan event is similarly problematic to the scroll event because it also fires off a lot of events. Notice that whenever I am updating the element’s style, I do so inside of the handler for the write method that is available through the DomController that was injected.

Click here to read the full tutorial.

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 things like requestAnimationFrame but Ionic 2 just makes it a lot easier.

Hopefully this also illustrates my point about how easy it is to make a bad application with Ionic: how is a beginner developer supposed to know that making 100’s of updates to the DOM 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 DOM write service, it would have a terrible impact on performance. I think 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”.

I don’t think beginner developers should be excluded because they may build bad applications or mocked for blaming the tool instead of their design. I think it’s fantastic that Ionic enables so many more people to build mobile applications, and naturally, these people will improve over time. I think we all just need to be mindful of important performance issues like this and support each other in addressing those issues.

Then hopefully one day we can shake this perception that HTML5 mobile applications are “just slow”.

What to watch next...

  • Sergey Rudenko

    Hey Josh, I am big fan of your web lessons;) thank you for doing all this! I have a question (I posted it to your email earlier) re domCtrl.write(). So this function seems to act similarly to requestAnimationFrame. Also based on this: https://github.com/ionic-team/ionic/blob/master/src/platform/dom-controller.ts – it should feature .cancel() functionality similar to requestAnimationFrame. It is very useful in dragging elements, where once user released button (endMove), animation can still have that extra frame going which normally we would want to cancel.

    I implemented requestAnimationFrame in one of my code snippets which works great (cancels exactly in the moment I need etc, prevent weird interaction glitches):

    background.move = function(event) {
    if (this.needForRAF) {
    this.needForRAF = false; // no need to call rAF up until next frame
    this.eventObj = this.isTouchSupported? event.touches[0]: event;
    this.moveCoordsCache = this.globalToLocalCoordinates(this.eventObj.clientX, this.eventObj.clientY);
    background.currentMove.x = this.moveCoordsCache.x – background.clickPoint.x;
    background.currentMove.y = this.moveCoordsCache.y – background.clickPoint.y;
    this.animReq = requestAnimationFrame(() => {
    background.globalScope.svgCanvas.setAttribute(“transform”, “matrix(1 0 0 1 ” + background.currentMove.x + ” ” + background.currentMove.y + “)”);
    background.globalScope.needForRAF = true;
    })
    }
    }.bind(this);
    background.endMove = function(event) {
    if (!background.globalScope.needForRAF) {
    console.log(“cancelling animation frame…”);
    cancelAnimationFrame(background.globalScope.animReq);
    background.globalScope.needForRAF = true;
    }
    background.ownerSVGElement.removeEventListener(this.moveEvent, background.move, {passive:true});
    background.ownerSVGElement.removeEventListener(this.endEvent, background.endMove, {passive:true});
    background.flattenAll(this.svgCanvas.transform.baseVal);
    }.bind(this);

    If i try to replace requestAnimationFrame with domCtrl.write() and domCtrl.cancel() – touch behavior is still glitchy as if I am not actually cancelling it. I wonder if you had experience with cancelling dom.write in Ionic and have an idea of how it should be implemented?

  • Sergey Rudenko

    hmm actually I just did replace RAF with domCtrl.write and cancel and it works great…I think I may have done soething wrong in the past;))) sorry!