Debugging layout repaint issues triggered by CSS Transition
A couple of weeks ago I was randomly checking the performance of a CSS Transition here on my blog. I was expecting to see a butter smooth animation but ended up surprised đ˛. The transition was triggering repaint on pretty much the whole page every time it ran.
The element being animated is a span
wrapping some text placed inside an h1
. The h1
itself is in the upper left corner on the page and contains my name. Initially, only the letter âDâ is shows. The remaining part fades-in on hover.
I was quite surprised to see the whole page flashing green given the transition was scoped to a very isolated element. I didnât really see any connection between animating a span
and causing repaint on the whole page.
So I went down a rabbit hole in attempt to figure this out.
What exactly am I animating?
My first stop was to double check the properties being animated:
h1 span {
display: inline-block;
opacity: 0;
transform: translateY(-20px);
transition: opacity 0.15s ease-out, transform 0.3s ease-out;
}
h1:hover span {
opacity: 1;
transform: translateY(0px);
}
As seen from the code, the transition is applied to opacity
and transform
, which are the recommended properties to use when moving and showing/hiding elements. Because of that, my first reaction was to dismiss the possibility that these properties are causing the problem.
There must be something else going on. đ¤
Could it be âlayersâ?
The next step was to see whether the pageâs content was split in any âlayersâ. This is usually not something I even think about but remembered that browsers can decide to place an item on a different âlayerâ in order to optimize the repaint of that element without requiring the rest of the layout to be repainted.
I used the âLayer bordersâ option in Edge DevTools for that but didnât really understand it. Also tried to use the will-change
property to force a new layer creation but that didnât help much either.
What about âstacking contextâ?
Next on the list was âstacking contextâ.
The MDN article explains this concept in more details but basically elements can be placed in different stacking contexts which are then stacked on top of each other. A new stacking context can be created by applying some special properties to an element. Some of those properties are position: relative
, opacity
value less than 1, transform
, etc.
In my case, the span
element contained opacity
and transform
. Other elements on the page with special properties were li
items used for wrapping each post link. They had position: relative
applied to them.
đĄ
Once I realized that the repaint issue might be due to a stacking context, I opened the â3D Viewâ panel in Edge DevTools to check:
By looking at the visualization, I could clearly see that the span
element (marked with z-index: auto
on the screenshot) was âbelowâ some other elements. What this meant in practice was whenever the element in the âlowerâ stacking context was repainted, the browser had to repaint the elements in the âhigherâ stacking contexts as well.
Solution
At this point the solution was a bit more obvious. I could either remove the position: relative
from the li
items, or add position: relative
and a z-index
to the span
element.
h1 span {
display: inline-block;
opacity: 0;
transform: translateY(-20px);
transition: opacity 0.15s ease-out, transform 0.3s ease-out;
position: relative;
z-index: 2;
}
After adding these two properties, the repainting issue was gone:
Sure enough, the 3D View also confirmed that the span
was moved to the topmost stack (marked with z-index: 2
in the screenshot below):
Iâm glad that I got to the bottom of this. This small optimization probably didnât make a huge performance difference on the page but it was definitely an interesting journey with a lot of learning.
So, at the end it wasnât the CSS transition itself that was causing the issue, but the elements and their stacking order.
Further reading
I also asked on Twitter for help to understand the problem better. Thankfully, Adam Argyle and @flackrw replied to my question and helped me a lot with their explanations.