Browser rendering optimizationby Theodor Tanase
In one of our recent projects, we had to develop a few games using web technologies, but we were limited by the computation power of the client machines (the games would have been used in a chromium-based browser).
To get a better idea about this, the final setup was pretty much similar to what a 90s’ game console would look like.
After testing various web frameworks (mostly canvas based) we decided to go with React. This was mainly because the component-based development proposed by React gave us the ability to better control which part(s) of the game we wanted to re-render at some point. We will cover the whole story about why we chose to React in a future article.
During our development, we’ve soon come to a dead-end when we had massive performance issues when rendering complex scenes (many moving objects, for example). This meant that we had only one chance to pull this off, optimizing all the scenes to a maximum.
Okay, so I have recorded my animations, there are plenty of red frames and my scripts are already running quite fast.
What else can I do?
Before making our app nice and fluid, let’s see the steps taken by the browser when it renders the HTML.
Here is what you can do to improve each phase of the rendering pipeline:
If there aren’t any new CSS rules, the browser will already know the style properties of each element. Therefore, we want to avoid adding or removing properties. This also applies to classes, since each class is a collection of style properties. What we want to do is changing a property’s value.
When calculating how much space an element needs, the browser looks at the element’s width and height, but also at its position in the tree. It is not recommended to create or delete elements dynamically, we can just hide the element if we don’t need it at some point. You can see how to hide and resize elements efficiently, bellow.
Not all CSS properties are created equal. Changing a property’s value may affect the Painting phase of the pipeline, while another may only intervene in the Compositing phase. We want to use properties that when changed, force the browser to take a few steps which are possible in order to re-render our element. Check the CSS Triggers list to see how many steps are affected by the property you were going to use, depending on the browser engine.
There is not much to be done in this phase. Whenever something is rendered, the browser must go through this step.
So, if we look at the CSS Triggers list, opacity and transform are our best friends. These properties only intervene in the Compositing phase, and you can use them to do pretty much anything.
There are some things we should note about transform. In order to make the rendering process easier, the browser organizes the elements in layers. If an element needs to go through layout re-calculations or repainting, it will only affect other elements in the same layer and children layers. Setting transform: “translateZ(0)” will force the browser to create a new layer for that element, reducing the number of siblings affected in case of a re-rendering. The same effect can be obtained with the will-change property.
So we can add transform: “translateZ(0)” everywhere in our HTML and we will have the most performant web interface, right?
Well, not really. The cool thing about layers is that they are GPU processed, meaning that the heavy work is transferred to the GPU. But the downside is that every new layer takes GPU memory, and even on the most performant desktops, a Chrome tab can use a maximum of 512MB of GPU memory. The key is to create new layers only for the elements being changed most frequently.
Now let’s see some practical tips and trick we can use.
- I want to hide an element – Use opacity: 0 instead of display: none
- I want to hide an element but it still takes space on my screen if I use opacity: 0 – Move it out of the screen using transform. For example, set transform: translate3d(2000px, 0,0)
- I really need to toggle a CSS class on an element – You could create two elements, one with the class set, and one without. You can now toggle their opacity.
- I would like to move an element to the left – Use translate3d(x,y,z) instead of changing left or top properties, which would affect the Layout phase.
- I would like to resize an element – We could use transform: scale(x,y), instead of altering width and height,
- I want to be able to play or pause an animation – Instead of toggling the animation property, we could use animation-play-state, which does not cause a repaint.
Hope you will find this useful, whether you develop a game or simply you want to get the best out of your web UI.