Search code examples
htmlcssgoogle-chromecss-transitionsgrid-layout

Transition causes repainting of all following elements in grid layout


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.


Solution

  • 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.