Lesson 2

Understanding Browser Rendering

How the browser takes your code and displays it on the screen

PRO

Lesson Outline

Understanding Browser Rendering

A lot of what we will be doing in this module focuses on how quickly we can make pixels appear on the screen. An Ionic application is really just a special type of web page that is displayed through a browser, and if we want to build high performing applications in Ionic then we need to know how the browser renders the application - we give the browser some code that describes how the application should look and function, and the browser is responsible for displaying that to the user.

We will be breaking down this process in even more detail throughout this module, but for now, we want to have a broad understanding of the basic concept. Our applications generally aren't just a static page, we have lots of moving parts that the user can interact with and change, so the browser needs to constantly update what is displayed to the user.

The basic process goes like this:

Frame Rendering Image from developers.google.com licensed under the Creative Commons Attribution 3.0 License

The browser works its way through this process every time it wants to display something on the screen. The result of this process is a frame that gets displayed to the user, which is a snapshot of what the application looks like at the current point of time. A "frame" of our application is similar to an individual frame/still image in a movie - the more frames there are the smoother the movie will look when those still images are played in quick succession.

This process should happen very quickly, and these "frames" will be output to the browser in quick succession. Ideally, we want to achieve a frames per second of around 60 for our application to feel high performing, but if the application is not designed well it can slow down this process leading to slow frames and a more sluggish feeling application. Using our movie analogy again, 60fps means having 60 individual still images/frames for every one second of the movie, which should make the movie feel smooth when these images are played. Like a movie, our application just gives the illusion of movement, but it is actually just a bunch of still images played one after the other. If we don't generate enough frames per second, then we lose this illusion of smooth movement.

In order to achieve 60 frames per second, we need to complete the entire process for rendering a frame every single time in around 10ms or 0.01 seconds. Browsers are quite powerful these days and can handle this, but not if we start doing things that are bad for performance. We will be looking at some examples in Ionic later that show how with a simple CSS change we can go from an animation that is slow and sluggish to one that is smooth and silky.

Outline

The Browser Rendering Process

Let's take a brief look at what each step in this rendering process does. Remember, the browser goes through the following process to calculate each frame, and ideally we want the browser to get through this process 60 times in one second (or we want the process to take no longer than 10ms each time is happens).

1. JavaScript/CSS

To kick off the first step in this process, we need something to happen that triggers a visual change. Most of the time this will be due to JavaScript that you write in your application: removing a list element, navigating to a new page, ticking a checkbox, animating an item. Typically it is JavaScript that triggers a visual change, but you could also trigger this step in the process with CSS.

At this point in time, some time will need to be spent doing the actual "work". If you are adding 100 new elements to your page, then JavaScript needs time to process all of that. If you are sorting an array, JavaScript will need time to process that. If you are calculating Pi to 32 trillion digits then JavaScript will need time to compute that. This first step of the process will be completed once all of the required scripting has finished.

2. Style

In this step, the CSS for all of the elements on the page are calculated. We may have multiple CSS selectors that apply to the same element, for example, we could have an <ion-item> that would have the following CSS rules applied to it:

ion-item {
  /* some styles */
}

.my-list ion-item {
  /* some styles */
}

.big-item {
  /* some styles */
}

The browser needs to figure out which selectors apply to which elements, and then combine those to figure out what the actual styles are for each element. We could have two different selectors apply to the same element, both setting the background-color, so the browser needs to determine which of those two selectors will "win" and thus which of those two rules would apply.

3. Layout

Once the browser knows the styles for each element, it can work out how big the elements should be and where they should be positioned on the screen. This is an expensive step because calculating the position of potentially every element on the screen in relation to all the others takes a while to figure out - the more elements you have the more difficult it becomes.

If we trigger some code that adds a new item to the top of the page then it is going to push all of the other items down the page - so the browser will need to calculate the new positions of every item on the page.

Even a simple CSS change could trigger this, if we were to add a margin-bottom to an existing element then that is also going to push down all of the other items and the new positions will need to be calculated.

4. Paint

This step is pretty much exactly what it sounds like - the browser knows what everything should look like, now it just has to paint it onto the canvas (I'm using a metaphor here, the result is not painted onto a literal <canvas> element). After all of the calculations have been performed the actual pixels can be "painted" onto the screen. Text is drawn, images are displayed, colours are added, borders are drawn, and so on. The browser does this "painting" on multiple layers that are placed on top of one another.

5. Composite

Since the browser paints onto multiple layers, it must display them in the correct order so that the application is displayed correctly. This step will ensure that if two elements are occupying the same space on the screen that the correct element will be displayed on top of the other.

Improving Browser Rendering

Our goal when attempting to improve the performance of our applications is to:

  1. Avoid as many of the steps above as possible
  2. Make the work that needs to be done inside of each step as efficient as possible

The biggest potential for improvement is generally to avoid triggering the Layout step wherever possible. It is an expensive step because everything on the page needs to be recalculated, and if you are doing something that is constantly triggering a layout you are going to have a tough time achieving 60 frames per second.

In an ideal situation, we could avoid both the Layout and Paint steps completely and only trigger a Composite. Since the composite step is responsible for organising the layers, if we were to just change the opacity of an element on its own layer to 0 there is no need to recalculate anything or repaint it, the composite step just needs to hide that one layer. Importantly, any changes we make to a layer (even moving its position on the screen) isn't going to create work for any other layer. However, not everything can be on its own layer so unfortunately this isn't a way to get great performance for free.

I don't want to dive too much into this now because we will be covering various aspects of this in-depth throughout the module, but I do want to highlight one example that should help illustrate the importance of understanding these concepts.

In a presentation on performance techniques at Ionic, Max Lynch (Ionic CEO) demonstrated the effectiveness of using CSS Containment on an element by adding the following CSS property to it:

contain: layout;

What this does is tell the browser that changes inside of this element aren't going to affect the layout of elements on the rest of the page. An Ionic modal is a good example of the usefulness of this property. If you display a modal on top of your page, and then you add something to that modal, there is no need to recalculate the positions of everything on the entire page - only the positions of the elements inside of the modal need to be recalculated. A modal kind of exists in its own little universe and it isn't going to affect anything else; CSS Containment is a way for us to explicitly tell the browser that.

In the example given, adding the contain property reduced the time taken to complete the Layout step from 56.90ms to 0.05ms which is an improvement of 1425x. This is a rather dramatic example, and a performance improvement that extreme is going to be rare, but there are plenty of small changes like this that can be made that will lead to a huge difference in performance.

Summary

The concepts that we have covered in this lesson are the basis for a lot of what we will be doing in this module. In future lessons, we will be going through various strategies for measuring and improving this process.

NOTE: We will be doing a lot of work using debugging tools in the browser throughout this module. All of the following lessons will primarily be using Chrome DevTools (Safari is very similar), sometimes using development builds in the browser and sometimes using production builds on a device. It is important to test using a production build (which we will discuss in the next lesson) at some point - a production build is heavily optimised and a development build is not - but there is still plenty of useful information to be gained from debugging development builds through the browser. However, when you are trying to profile your application and debug performance issues, it is wise to do that with a production build.

I will assume you are familiar with how to launch remote debugging tools while on a device for both iOS and Android, however, if you are not you can find additional videos on doing that here: