Solution 1 :

A CSS only solution where I will animate only opacity and tranformation. Should use less ressources. Not as perfect as the SVG one but good enough as an alternative:

.outer {
  border-radius: 5px;
  position: relative;
  width: 300px;
  height: 300px;
  border: 1px solid gray;
  margin:15px;
  clip-path:inset(-5px);
}
.outer::before,
.outer::after,
.outer span::before,
.outer span::after {
  content:"";
  position: absolute;
  opacity:0;
  transition:1s;
  animation: 1s linear infinite;
}
.outer::before,
.outer::after {
  height: 2px;
  left: -5px;
  width: calc(100% + 20px);
  background: repeating-linear-gradient(to right, red 0 5px, transparent 0 10px);
  animation-name:drawX;
}

.outer span::before,
.outer span::after {
  width: 2px;
  top: -5px;
  height: calc(100% + 20px);
  background: repeating-linear-gradient(to bottom, red 0 5px, transparent 0 10px);
  animation-name:drawY;
}

.outer::before {top: -5px; animation-direction:reverse;}
.outer span::before {right: -5px; animation-direction:reverse;}

.outer::after { bottom: -5px;}
.outer span::after { left: -5px;}

.outer:hover::before,
.outer:hover::after,
.outer:hover span::before,
.outer:hover span::after {
  opacity:1;
}

@keyframes drawX {
  to {
    transform: translateX(-10px);
  }
}
@keyframes drawY {
  to {
    transform: translateY(-10px);
  }
}
<div class="outer">
  <span></span>
</div>

With Some CSS variables to control everything:

.outer {
  --th:2px; /* border thickness */
  --w:5px; /* width of the color*/
  --s:5px; /* the space between color*/
  --o:5px; /* the offset */
  --c:red;
  --g: var(--c) 0 var(--w), transparent 0 calc(var(--w) + var(--s));
  
  border-radius: 5px;
  position: relative;
  width: 200px;
  height: 200px;
  display:inline-block;
  border: 1px solid gray;
  margin:15px;
  clip-path:inset(calc(-1*var(--o)));
}
.outer::before,
.outer::after,
.outer span::before,
.outer span::after {
  content:"";
  position: absolute;
  opacity:0;
  transition:1s;
  animation: 1s linear infinite;
}
.outer::before,
.outer::after {
  height: var(--th);
  left: calc(-1*var(--o));
  width: calc(100% + var(--w) + var(--s) + 2*var(--o));
  background: repeating-linear-gradient(to right, var(--g));
  animation-name:drawX;
}

.outer span::before,
.outer span::after {
  width: var(--th);
  top: calc(-1*var(--o));
  height: calc(100% + var(--w) + var(--s) + 2*var(--o));
  background: repeating-linear-gradient(to bottom, var(--g));
  animation-name:drawY;
}

.outer::before {top: calc(-1*var(--o)); animation-direction:reverse;}
.outer span::before {right: calc(-1*var(--o)); animation-direction:reverse;}

.outer::after { bottom: calc(-1*var(--o));}
.outer span::after { left: calc(-1*var(--o));}

.outer:hover::before,
.outer:hover::after,
.outer:hover span::before,
.outer:hover span::after {
  opacity:1;
}

@keyframes drawX {
  to {
    transform: translateX(calc(-1*(var(--w) + var(--s))));
  }
}
@keyframes drawY {
  to {
    transform: translateY(calc(-1*(var(--w) + var(--s))));
  }
}
<div class="outer">
  <span></span>
</div>

<div class="outer" style="--th:4px;--o:8px;--s:8px;--w:8px;--c:blue;">
  <span></span>
</div>

Problem :

I have a very simple border created with a SVG path. If I try to animate it with it’s stroke-dashoffset property, the GPU usage of Chrome spikes up to 15%. This seems excessive given the fact that I’m animating a single stroke.

Why is this happening? If this resource usage is expected, are there any alternative ways with which I can create the same effect while keeping the resource usage lower?

Demo: Place the cursor on top of the box in order to animate the svg path (and check the GPU usage of chrome)

.outer {
  border-radius: 5px;
  position: relative; 
  width: 300px;
  height: 300px;
  border: 1px solid gray;
}

.border-path {
  width: calc(100% + 10px);
  height: calc(100% + 10px);
  position: absolute;
  top: -5px;
  left: -5px;
  stroke-width: 5;
  stroke-dasharray: 8;
  stroke-dashoffset: 1000;
  stroke-opacity: 0;
  fill: transparent;
}

.outer:hover > .border-path {
  stroke: red;
  stroke-opacity: 1;
  animation: draw 30s linear infinite forwards;
}

@keyframes draw {
  to {
    stroke-dashoffset: 0;
  }
}
<div class="outer">
  <svg class="border-path">
    <rect x="0" y="0" width="100%" height="100%" />
  </svg>
</div>

Comments

Comment posted by Michael Mullany

SMIL animation has the same GPU usage – just tried it. Might want to try it with Javascript setTimeout animation and see what happens – should probably get higher CPU and lower GPU burn.

Comment posted by Temani Afif

are you intresting in a CSS only approach to achieve this where you can rely on opacity/transformation only? Not as good as SVG but can be a good alternative

Comment posted by alexandernst

@TemaniAfif I’m interested in whatever method can achieve the same visual effect while using less CPU / GPU resources.

Comment posted by alexandernst

I’m testing this and it does manage to reduce the CPU / GPU usage by 2 o 3%. I’m not really sure what the “clip-path” is doing though

Comment posted by Temani Afif

@alexandernst the clip-path is to hide the overflow, since I need to make the pseudo element a litte bigger in order to translate them (remove it to understand)

Comment posted by alexandernst

I’m revisiting this. Do you happen to know why is this happening? Drawing an animated path around a square doesn’t sound like a valid reason for Chrome to use 1/6 of the GPU power of my machine. Maybe something is wrong / buggy in Chrome’s renderer?

Comment posted by Temani Afif

@alexandernst I cannot give you an accurate answer since I don’t know how things work behind the scene but you can try to remove/add things to indentify the culprit. Start with one edge have a solid color (not a gradient) then start adding other propertie one by one until you reach the final result and compare to see when it’s getting crazy

By