skip to Main Content

I would like to know the reason and the solution to the excessive repaint in the following HTML document. On Chrome, repaint can be visualized in devtool -> rendering panel -> Paint flashing. The problem itself has many ways to workaround, but I’m more interested in the reason why such repaint happens.

for (const el of document.querySelectorAll(".contents > img")) {
  el.addEventListener("mouseenter", function() { this.classList.add("easy-in-out", "scale-150", "scale", "z-20"); });
  el.addEventListener("mouseleave", function() { this.classList.remove("easy-in-out", "scale-150", "scale", "z-20"); });
}
<div class="grid grid-cols-2" style="width: 410px">
  <div class="contents"><img class="relative transition p-1" src="https://picsum.photos/200"></div>
  <div class="contents"><img class="relative transition p-1" src="https://picsum.photos/200"></div>
  <div class="contents"><img class="relative transition p-1" src="https://picsum.photos/200"></div>
  <div class="contents"><img class="relative transition p-1" src="https://picsum.photos/200"></div>
</div>
<script src="https://cdn.tailwindcss.com"></script>

(However, it seems that Chrome is not capable of showing "Paint flashing" inside the iframe, so I also provide the minimal single-file snippet below, or online at https://jjyyxx.github.io/grid-repaint/)

<!DOCTYPE html>
<html>
<body style="width: 410px;">
  <div class="grid grid-cols-2">
    <div class="contents"><img class="relative transition p-1" src="https://picsum.photos/200"></div>
    <div class="contents"><img class="relative transition p-1" src="https://picsum.photos/200"></div>
    <div class="contents"><img class="relative transition p-1" src="https://picsum.photos/200"></div>
    <div class="contents"><img class="relative transition p-1" src="https://picsum.photos/200"></div>
  </div>
  <script src="https://cdn.tailwindcss.com"></script>
  <script>
    for (const el of document.querySelectorAll(".contents > img")) {
      el.addEventListener("mouseenter", function() { this.classList.add("easy-in-out", "scale-150", "scale", "z-20"); });
      el.addEventListener("mouseleave", function() { this.classList.remove("easy-in-out", "scale-150", "scale", "z-20"); });
    }
  </script>
</body>
</html>

When you mouseleave one image element, the fading transition will cause all its following image elements repainted. With many images or some large images, this behavior causes frame drop.

Since the layout did not change, in my opinion, only the hovered element should get repainted. If style="will-change: scale;" is added to divs with selector .contents > img, this desired behavior could be achieved. But the documentation suggests that using will-change extensively is not a best practice.

Reproducible on Chrome v120.0.6099.72. Removing relative on image elements seems to fix this repaint.

2

Answers


  1. You can add transform-gpu to all the <img> tags:

    <img class="relative transition p-1 transform-gpu" src="">
    

    This will put them in separate layers and will use GPU for transformations.

    Login or Signup to reply.
  2. The issue is that Chrome is grouping parts of your page into larger layers, which normally helps with performance as it has less parts of the page to track. The downside is that if you animate part of one of these layers then the whole layer gets repainted.

    The different workarounds are all forcing Chrome to separate out the elements you apply them to into their own smaller layers. There is obviously a performance trade off when you do this. One work around is to only apply these CSS fixes to the element while the transition is running, and then remove it when it has completed.

    You might find this article interesting that goes into every detail of things that cause janky rendering.

    https://www.sderosiaux.com/articles/2015/03/01/perfmatters/

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search