...we're probably doing some really stupid things in CSS we don't know about…
This is a sentence from around 2009 said by Douglas Crockford and, despite the context where it was said, I would like to use it as a starting point for this article.
A few years ago, browsers were just something used to display text, tables, forms, images and occasionally some more complex graphics in the form of animated GIFs.
We were used to learning CSS, JS and HTML and worry mostly about compliance rather than performance. This led us to do things in a certain way without actually knowing why or even if they were correct. We did it simply because it worked.
We didn’t have the tools to learn this kind of stuff and even more importantly, we didn’t have the need to learn this kind of stuff. But everything starts changing when 3 main things start happening: modern browsers, mobile and modern web.
Mobile, by its side, pushed us to be more careful with the amount of stuff that we were loading on screen and with that, forced us to do our code differently by taking into account the slow speeds of the first mobile phones.
Finally, the modern web was responsible for the incredible and sometimes crazy stuff that we are “forced” to implement nowadays and impelled us to the point that, if we just technically know a way of doing something without actually understanding why, probably the result is going to be a lousy animation or a lousy interaction.
So the goal with this article is to show solutions and demonstrate that, by making some inoffensive mistakes, we can create so much damage to a web application to the point where because it takes so much time to load, some potential customers could go away and do their shopping elsewhere.
How a Browser Renders a Page
Before Jumping into the main topic of this article, first we need to understand how the browser renders a page.
Lets resume the steps:
1. As soon as the user enters the URL, the browser fetches the HTML source code from the server and then parses that HTML and convert it into the tokens we all know and love, like <head>, <body>, <div> and in the end the tokens are converted into nodes. Only then we have the DOM Tree;
2. After the DOM Tree, it’s time for the CSSOM Tree to be generated from the CSS file. A lot is done here but we don’t need to get into details;
3. Finally, the DOM and CSSOM trees are combined into a single RenderTree;
- The RenderTree is constructed following the next steps:
I - It starts from the root of the dom tree and compute which element is visible and it’s computed styles;
II - It ignores the not visible elements like (meta, script, link) and the ones with “display: none;”;
III - Matches each visible node to the appropriate CSSOM rules and applies them;
4. Only after all these steps are done the browser starts to bother with how to display (Reflow) and style (Repainting) things on screen;
By definition, “Reflow” is the process the web browser uses to re-calculate the positions and geometries of elements in the document. It’s basically the effort the browser needs to do to position every element in the screen every time something changes (either by a refresh, a screen resize or a common show/hide content action).
The Reflow happens when the changes made to the elements affects the layout of a portion of the page or the whole page. The Reflow of an element will cause the subsequent reflow of all the child and ancestor elements in the DOM.
Also important to know is that a reflow operation is a user blocking operation, which means the user cannot interact with the page until reflow is finished.
“Repaint” is the definition given to the process where the browser starts giving the right appearance to the elements that were placed in the Rendered Tree such as background colours or font-sizes.
Repainting also happens in situations such as showing an hidden element, animating a node or changing a text colour.
What may cause Reflow and Repaint
Bellow we can see in detail some more common tasks that may affect reflow, repaint or both:
Change CSS properties like padding/margin/border: Reflow and Repaint;
Changing colours like a background or border colour: Repaint;
Changing a font-size: Reflow and Repaint;
Append/remove an element to DOM: Reflow and Repaint;
Hiding DOM elements with “display: none;”: Reflow and Repaint;
Hiding DOM Element with “visibility: hidden;”: Repaint;
Window resize: Reflow;
Add/remove a stylesheet: Reflow and Repaint;
Window scroll: Reflow (Affects only if a layout change is
made to the page) and Repaint;
Calling JS getComputedStyle(): Reflow;
Getting windows dimensions with JS: Reflow;
Using scroll() functions from JS: Reflow;
Getting box metrix with JS: Reflow;
Setting an element focus using “element.focus()”: Reflow;
Typing text in an input box: Reflow and Repaint;
Use style attribute: Reflow and Repaint;
Manipulating the class attribute: Reflow and Repaint (Affects only if the class change some visuals to the page);
So as we easily understand, there is a lot of action that we normally do that may be leading a user to say that their app is sluggish. And for most of these actions, I risk to say that a huge percentage of us didn't even know they were responsible for that. As I said before, sometimes we do things simply because we know they work.
Before going further and talk about techniques that we can follow to minimise these impacts, let's just sit a little more and think about a couple of the items I listed in the table above.
Calling JS getComputedStyle(), window dimensions, scroll() or box metrics
Modern browsers are getting smarter, or at least they start trying to fix the poor code that us humans do. For instance, modern browsers can set up a queue of the changes that your scripts require and perform them in batches. This way, if we have several changes each requiring a reflow, they could be combined and only one reflow is needed. Naturally, browsers can add requests to the queue and then flush the queue once a certain amount of time passes or a certain number of changes is reached. It’s up to them to decide.
This is a very good optimization mechanism if it weren't for the fact that we, human developers, can inadvertently block this from happening.
Sometimes our scripts may prevent the browser from using this queue of changes, causing it to flush that queue and perform all batched changes. Generally is something that happens when we request a style information like the ones we are talking about (getting offsets, using scroll stuff).
Let's say that we have a script that needs to change the top value of an element based on a position in the screen:
What is happening here is that the script, by requesting the “offsetTop” information from the browser, is saying “hey, please give the most updated information…NOW” and the browser, because it’s so well ordered, to give that information as updated and accurate as possible, first needs to flush the entire queue, run all tasks and then reflow/repaint the screen so finally he can send the right information back to the script.
Apparently we are only adding one reflow request but in fact, we could be triggering much more than that.
Now imagine having these types of requests happening in succession, like inside a loop... mind blows! Avoid this or at least, minimise it’s usage.
How to minimise repaints and reflows
The question in our minds may presumably be “What can we do to reduce the number of reflows and repaints?”
In the next few minutes, I will try to give you some tips on how to avoid reflows and repaints and also, when that is not possible, ways of reducing the number of changes to the page.
Reduce DOM depth
Reducing DOM depth means that, maybe you don’t need to have those 20 <div> and <span> elements all children of the same parent element. When a reflow occurs, the DOM tree nodes above and below of the node that was changed will also change (reflow) and naturally the more nodes we have in our tree, the more effort the browser will need to spend.
Have a clean CSS sheet
Since repainting means time and resource consumption for the browser, having as few properties and attributes as possible is one good way of helping the browser show stuff on screen. Remember to remove all styles from your CSS sheet that you don't need.
Also, keep in mind to not abuse too much of CSS selectors since they cost more CPU power.
Don't change individual CSS styles
A lot of times, we need to change a bunch of styles when a user performs an action. In this scenario, it is best if we just apply a class to the element instead of changing 3 or 4 CSS styles.
In this example, we are adding 2 reflows (bad) instead of just 1 (better).
Another cool tip about this i, if we have a component that may have a lot of changes, it could be best to apply a class not to the wrapper element but instead to a more specific element where the change is going to happen. This could help the browser to manage better the amount of the RenderTree that he needs to reflow.
Batch DOM changes
In some scenarios we may need to change a lot of elements in our RenderTree. For this we can go with two options:
- Clone the node that you need to change, apply all the changes you need and then replace the original one. The same is valid if you are creating and appending a new node to the DOM;
In this example, we are adding 1 reflow and 1 repaint.
- Change the visibility of the node you need to change using “display: none;”, make your changes and then restore the display.
In this example, we are adding 2 reflows and 2 repaints.
Avoid to much computed styles
Every time we ask for a computed style like “.offsetTop” or “.clientHeight” the browser needs to make a huge effort to retrieve the correct value. Avoiding this type of requests is one massive improvement you can apply to your code.
Basically just request the information you need once, save the result and iterate over it.
Avoid inline styles
As we saw, setting styles via the style attribute causes reflows and by consequence, adding multiple styles, even on the same element, each one could be causing a reflow. If possible, combine all the styles in one single class.
Use absolute/fixed positioning
Having beautiful animations in our apps is amazing. But they are worth much less if only those users with high range phones can see them working perfectly. If we don’t take in account a couple of best practises to improve performance, most users will get glitchy animations.
Usually, what causes the animating of an element to be slow and glitchy is exactly the amount of effort the browser needs to put in while reflowing and repainting the screen.
In short terms, we can give more “processing power” to the browser by using the GPU - there is a lot to say about this but maybe in another article - or we can use some tricks to avoid excessive “reflowing” and “repainting”.
One of the tricks that we can use to improve our CSS performance is to use absolute or fixed positioning on the elements that will change. By doing this, animating an element will not affect other elements in the layout since the element will be “a child” of the document parent element and will only require a reflow for a portion of the page and not a full DOM reflow, which is a lot less “costly” for the browser.
Reduce the “unnecessary” smoothness
Usually, when we need to animate an element on the screen, e.g. moving it vertically, we can move it 1px at a time, but for longer animations it may cause the CPU to start struggling to deal with the amount of reflows and subsequent calculations. Instead, we can move the element 3px or 4px at a time reducing drastically the amount of reflows and calculations that the CPU needs to perform.
With this, we are not losing smoothness in faster devices and for older devices, we are making sure that the animation is not going to break the user experience by making the device slower.
Tools to help us
Lucky for us developers, today we have a variety of tools that can help us understand the impact of our code and the “pain points” that we may be creating.
Browser Dev Tools (inspector)
The most obvious tool is the inspector tool that is available in any modern browser.
The browser inspector can show us how the page is being rendered, which and when elements are being rendered and also the amount of reflows and repaints that may be occurring.
The examples that follow are Chrome based but for instance, Firefox does a pretty good job on this also.
In this example, I am using a page from Wikipedia. We are looking at a portion of the entire loading process of that page.
To get this information, we only need to perform a couple of steps:
- Open the inspector tools (hit F12 or left click the page and select “Inspect”);
- Access the TAB named “Performance”;
- Click the first icon on (the round dot) to start record (a popover will be visible indicating that a recording is underway);
- Refresh the page that we are inspecting;
- Hit “Stop” in the inspector popup to stop recording and process the data.
When your recording is finished processing, by clicking the “Summary” TAB in the bottom you can see a resume of what happened during the timeframe that you record.
Here we can learn that between reflow (rendering) and repaint (painting) more than 250ms have passed.
Next, in the bottom half of the screen, we can see all the events that have to do with reflowing and repainting.
And if we dig even further into a single layout event, we can find out the amount of nodes and time needed to perform that request.
- This layout change request took more than 25ms do finish;
- It affected almost 1119 nodes in the DOM;
- It affected the entire DOM tree (#document)
In the middle section of the inspector, we can see the events in sequence and have a visual representation of the timeline. What we can learn from here is that during this time, the page is blocked (user interactions are blocked to prevent input responsiveness) meaning that the user couldn’t interact with the page
If we start generating a lot of these events, we can easily understand the final result.
From what I found by searching for more tools, there is nothing that can surpass the dev tools from the modern browser. You have a couple of browser extensions that can help analysing your DOM performance but still, dev tools are the best.
- Lighthouse Extension (for Chrome, Firefox, Edge);
For the conclusions I would like to travel back to the beginning of this article and more particularly to the subtitle.
Make web faster instead of silk and smooth
I’m not saying to go faster instead of go beautiful. What I am saying here is that performance should be our number 1 priority and even if we cannot deliver the perfect interaction to the users, that is usually fine because for them what matters the most is the effort and time they need to spend to accomplish a task.
Personally, I am tired of seeing incredible apps and web sites with super modern and shiny UI but then they fail to deliver what matters. A fast response to my actions. When experiencing an app I want to do things as quickly as possible, not as pretty as possible.
Check the full process on our Masterclass#7 video.