How to Write Better CSS with Performance in Mind

In today’s post we will ponder over the code choices we can make in CSS for improved site performance. But, before we dive into those choices, let’s first take a brief, closer look at the webpage rendering workflow in order to focus on the problematic (performance-wise) areas that are solvable via CSS.

Browser Rendering Workflow

Here’s the rough flow of operations performed by the browser after DOM tree creation:

  1. Recalculate Style (and render tree creation). Browser computes the styles to be applied to the elements in the DOM tree. A render tree is later created while discarding the nodes (elements) from the DOM tree that aren’t to be rendered (elements with display:none) and those that are (pseudo-elements).
  2. Layout (aka Reflow). Using the computed style from before, the browser calculates the position and geometry of each element on the page.
  3. Repaint. Once the layout is mapped, pixels are drawn to the screen.
  4. Composite Layers. During repainting, the painting might be done in different layers autonomously; those layers are then finally combined together.

Now let’s continue on to what we can do in the first three stages of the operation to write better-performing CSS codes.

1. Reduce Style Calculations

Like mentioned before, in the "Recalculate Style" stage the browser computes the styles to be applied to the elements. To do this, the browser first finds out all the selectors in the CSS that point to a given element node in the DOM tree. Then it goes through all the style rules in those selectors and decides which ones are to be actually applied to the element.

Style Calc
IMAGE: Aerotwist

To avoid costly style calculations, reduce complex and deeply nested selectors so that it’s easier for the browser to figure out which element a selector is referring to. This reduces computational time.

Other ways to employ include reducing the number of style rules (where possible), removing unused CSS and avoiding redundancy & overrides, so that the browser doesn’t have to go through the same style again and again during style calculations.

2. Reduce Reflows

Reflows or Layout changes in an element are very "expensive" processes, and they can be of an even bigger problem when the element that went through the layout changes has a significant amount of children (since Reflows cascade down the hierarchy).

Reflows are triggered by layout changes to an element, like changes to the geometric properties such as height or font size, the addition or removal of classes to elements, window resizing, activated :hover, DOM changes by JavaScript, etc.

Just like in style calculation, to reduce Reflows, avoid complex selectors and deep DOM trees (again, this is to prevent excessive cascading of Reflows).

If you have to change the layout styles of a component in your page, target the styles of the element that is at the lowest in the hierarchy of elements that the component is made of. This is so that the layout changes doesn’t trigger (almost) any other Reflows.

If you’re animating an element that goes through layout changes, take it out of the page flow by absoutely positioning it, since Reflow in absolutely positioned elements won’t affect the rest of the elements on the page.

To summarise:

  • Target elements that are lower in the DOM tree when making layout changes
  • Choose absolutely positioned elements for layout changing animations
  • Avoid animating layout properties whenever possible

3. Reduce Repaints

Repaint refers to the drawing of pixels on the screen, and is an expensive process just like Reflow. Repaints can be triggered by Reflows, page scroll, changes in properties like color, visibility and opacity.

Pixel Art

To avoid frequent and huge repaints, use less of the properties that cause costly repaints like shadows.

If you’re animating properties of an element that can trigger Repaint directly or indirectly, it’ll be of great advantage if that element is in its own layer preventing its painting prcoess from affecting the rest of the page and triggering hardware acceleration. In hardware accelaration, the GPU will take up the task of performing the animation changes in the layer, saving the CPU extra work while speeding up the process.

In some browsers, opacity (with a value of less than 1) and transform (value other than none) are automatically promoted to new layers, and hardware acceleration is applied for animations and transitions. Preferring these properties for animations is thusly good.

To forcefully promote an element to new layer and go into hardware acceleration for animation, there are two techniques invovled:

  1. add transform: translate3d(0, 0, 0); to the element, tricking the browser into triggering the hardware acceleration for animations and transitions.
  2. add the will-change property to the element, which informs the browser of the properties that are likely to change in the element in the future. Note: Sara Soueidan has an in-depth and super-helpful article on this in the Dev.Opera site.

To summarise:

  • Avoid expensive styles that cause Repaints
  • Seek layer promotion and hardware acceleration for hefty animations and transitions.

Take Note

(1) So up til now, we haven’t touched on CSS file size reduction. We have mentioned that reduction in style rules (and DOM elements) make a significant performance improvement because of how much the browser will have to work less on the process of computing the styles. As a consequence of this code reduction, writing better selectors and the deletion of unused CSS, the file size will automatically decrease.

(2) It’s also advisable to not make too many consequential changes to an element’s styles in JavaScript. Instead add a class to the element (using JavaScript) that holds the new styles to make these changes – this prevents unnecessary Reflows.

(3) You will want to avoid Layout Thrashing as well (forced synchronous Reflows) which arises due to the accessing and modifying of the Layout properties of elements using JavaScript. Read more about how this kills performance here.