The process of a web browser turning HTML, CSS, and JavaScript into a finished visual representation is quite complex and involves a good bit of magic. Here’s a simplified set of steps the browser goes through:
- Browser creates the DOM and CSSOM.
- Browser creates the render tree, where the DOM and styles from the CSSOM are taken into account (
display: none
elements are avoided). - Browser computes the geometry of the layout and its elements based on the render tree.
- Browser paints pixel by pixel to create the visual representation we see on the screen.
In this article, I’d like to focus on the last part: painting.
All of those steps combined is a lot of work for a browser to do on load… and actually, not just on load, but any time the DOM (or CSSOM) is changed. That’s why many web developers tend to partially solve this by using some sort of frontend framework, such as React which, apart from many other advantages, can help to highly optimize changes in the DOM to avoid unnecessary recalculating or rendering.
You may have heard terms such as state, component rendering, or immutability. All of those have something to do with the optimization of DOM changes, or in other words, to only make changes to the DOM when it’s necessary.
To give an example, the state of a web application may change, and that would lead to a change in UI. However, certain (or many) components are not affected by this change. What React helps to do is limit the writing to the DOM for elements that are actually affected by a change in state and ultimately limit the rendering to the smallest part of the web application possible:
DOM/CSSOM → render tree → layout → painting
However, browser painting is special in its own way, as it can happen even without any changes to the DOM and/or CSSOM.
The diagram above was generated using Chrome’s performance panel in DevTools (more on that later) and it shows how much time was taken by each task in the browser in the recorded time (0-7.12s) after reloading of a page. As you can see, painting takes a significant part, and that’s not automatically a bad thing. In this particular example, the increased painting is caused by a combination of animated GIFs on the page and canvas drawing (at 60fps), where both don’t cause any changes to the DOM or its styles, while still triggering painting.
Another good example of a feature that may cause painting without any outside intervention is the CSS animation
property, and compared to animated GIF or canvas, it is probably more common on the web. An animation is usually triggered by user input, like hover, but thanks to animation
and @keyframes
rules, we can even create quite complex animations running constantly on the page without much of an effort, which is pretty amazing.
What some might not realize, is that those animations can easily get out of hand and constantly trigger painting, and that can cost us a lot of processing power. Of course, there are some rules that can be used to avoid painting. Most obvious is limiting manipulation of elements to CSS transform
and opacity
properties, which by default don’t trigger paint, unless some special circumstances are in place, such as animating an SVG path.
Paint flashing
You likely know that Chrome has DevTools. What you might not know about is a little shortcut (Shift+Cmd+P on Mac or Control+Shift+P on PC) which can be used inside DevTools to bring up a little search bar and command menu.
I’ve started digging around it, and apart from many other useful and incredibly interesting options, a render panel caught my attention.
At the first sight, you can see some interesting options that can be very helpful when it comes to debugging animation on the web, like an FPS meter.
Layer borders and paint flashing are also interesting tools. Layer borders are used to display the borders of layers as they are rendered by the browser so that any transformation or change in size is easily recognizable. Paint flashing serves to highlight areas of the webpage where the browser is forced to repaint.
After discovering paint flashing, the first thing I did was check it out on a project of mine. In most places, there was no trouble. For instance, any movement triggered by scroll on the website was powered by the CSS transform
property, which as we covered, doesn’t cause painting. The painting was present where one would expect it to be, like changes in text color on hover, but that’s not something that should be much of a concern due to its area and presence only on hover of the element. To sum it up, you can always find something to improve, even if you wrote the code yesterday…
But one thing was a slap in the face.
It doesn’t matter how experienced or careful you are, you can — and most likely will — make a mistake. We’re just people and some would argue that fixing your own bugs is most of the job when it comes to development. However, for a bug to be fixable, we need to be aware of it… and that’s exactly where the render panel helps.
Case study
Let’s take a closer look at the actual issue. The design came in with the request for a noisy background. That kind of effect that old TVs had when there was no signal.
It is known that GIFs have many issues, where performance is certainly one of them, so I definitely couldn’t use that for a whole page background. If you’d like to read some more on why to avoid GIFs, here is a good resource with a bunch of reasons.
Using JavaScript is definitely an option in this case. Displaying or hiding elements with a slightly moved background was the first thing that came to my mind, and using canvas could help too. However, all of this seemed a little overkill for simply having a background. I decided to go for a CSS-only approach.
My solution was to take a small “noisy” PNG image as a background-image
, enable background-repeat
and throw it over a one-color background. How did I achieve the noise effect? With infinite CSS animation! By setting the background-position
to different value over the period of 200 milliseconds. Here’s how that turned out:
See the Pen MXoddr by Georgy Marchuk (@gmrchk) on CodePen.
Can you guess the problem? It seemed like a quite an elegant solution to me, and I was excited about my achievement of making it through without a crappy GIF and even not a single line of JavaScript. Just simple CSS that is optimized in browsers these days.
Well, the paint flashing showed something completely different. The layer of the size of the window was constantly repainting, without the user even doing anything. You can see the paint flashing in the demo above if you enable it in the render panel (note that paint flashing doesn’t show up in embedded pen).
That certainly doesn’t play well with the performance of the website and drains laptop batteries like there’s no tomorrow.
All of this CPU usage could have been avoided by replacing the changes to background-position
using transform
or opacity
.
See the Pen XYOYGm by Georgy Marchuk (@gmrchk) on CodePen.
The problem
I’ve been doing web development for a while and I knew very well that animating a background is never a good idea. This felt like a rookie mistake. People make mistakes… but that’s not the whole story. The website was all laggy and uncomfortable to navigate. How did I miss it?
Something that certainly plays a big role is the fact that I am (and you may be as well) a little spoiled when it comes to development equipment. I have a nice, powerful computer for work and access to speedy internet. Unless we write some really crappy code, anything we write runs quite smoothly in our eyes. But that’s not always the case for our users.
A similar problem applies to many other things — like display size. Using a little exaggeration, while we are developing on 27” display with 4K resolution and getting the designs primarily for 1920×1080, our visitors come in mainly from 1366×768 laptops and have a completely different workflow when it comes to using a computer.
Conclusion
While this article started off as a piece about painting, its main topic is really much more about being mindful of the impact our code has on the painting process or performance in general. While painting serves as a good example of something that can be problematic and easily missed, it’s more of a disconnect between developer and user that is the issue.
The web is a place of many environments, where the developer’s environment is often far different than the user’s. While there is no need to change our ways or switch to lazy computers, it definitely helps to see our work the way it is seen by others from time to time. My suggestion is: when you come home from work and have a little free time, try to pick up your old computer and check your work there, to get a little closer to what your users experience.
If you don’t have this kind of computer around, tools like render panel can turn out to be awfully handy.