Using steps() for performant CSS transitions

I've used a lot of CSS transitions over the years. Shifting between values in a smooth fashion is pretty easy with a simple one-liner.

transition: background-color 2s;

But what if you have a lot of transitions? What if you have too many transitions? Your browser tries to run them as fast as your monitor's refresh rate allows (60 times per second, or more). This can be pretty taxing.

Recently I ran into this problem transitioning many elements of a large SVG with a bunch of complex shapes. With all the elements transitioning all at once, the page scrolling started to stutter quite noticeably.

Here's how I solved it. Instead of a transition, I used CSS animations, with steps() to effectively "bring down the frame rate" and stop the browser from doing too much work.

@keyframes color-change {
  100% {
    fill: rgb(12, 169, 210);
  }
}

.do-transition {
  animation-name: color-change;
  animation-duration: 5s;
  animation-iteration-count: infinite;
  animation-timing-function: steps(10);
}

Or you can use the shorthand.

.do-transition {
  animation: color-change 5s infinite steps(10);
}

Just add the do-transition class to the element you want to animate. Adjust the animation-duration and the steps() count to get a balance between smoothness and performance.

const polygon = svg.querySelector("polygon");
polygon.classList.add("do-transition");

Here's a little demo with a simple SVG. I've kept the steps() low to make the effect more noticeable.

That's the basic idea anyway. I hope it helps if you run into a similar problem.

(Bonus tip: Use animation-delay to stagger the animations if you have a lot of elements transitioning at once so they don't all paint on the screen at once on each step())

Happy coding! 🚀

A brief interlude into Gleam concurrency

I've been learning Rust for a while now.

The other day I came across this little project called Gleam. It's a functional programming language. It's written in Rust. It compiles to JavaScript and Erlang. It borrows a lot of the good bits from Rust while hiding away a lot of the lower-level stuff. It feels kinda like Rust-lite.

I was intrigued, so I took a quick look into it.

If you're also intrigued I recommend taking the Gleam language tour, or keep reading for a quick speedrun.

Gleam is easy to install via Homebrew or your system package manager.

New projects can be made with gleam new my_project.

Packages can be added with gleam add. Let's add gleam_otp with gleam add gleam_otp, which we'll use to spawn new tasks. It will now be an entry in your gleam.toml file, under [dependencies]

gleam_otp = ">= 0.10.0 and < 1.0.0"

I'm interested in Gleam's concurrency handling, so let's test it out. Add the following code to src/my_project.gleam (an example from the Gleam homepage).

import gleam/int
import gleam/io
import gleam/list
import gleam/otp/task

fn spawn_task(i) {
  task.async(fn() {
    let n = int.to_string(i)
    io.println("Hello from " <> n)
  })
}

pub fn main() {
  // Run loads of threads, no problem
  list.range(0, 200_000)
  |> list.map(spawn_task)
  |> list.each(task.await_forever)
}

Compile and run the code with gleam run.

You should see a whole bunch of hellos from different numbers. They appear, at first glance, to be counting in order from 0 to 200,000 like this.

Hello from 44790
Hello from 44791
Hello from 44792
Hello from 44793
Hello from 44795
Hello from 44794
Hello from 44796
Hello from 44797
Hello from 44798
Hello from 44799

The cool thing is though, that each of these tasks is being run concurrently (if you've got a multi-core processor that is ... I think) ...

... And if you scroll back up, you should see that some of the numbers are actually out of order. This is because sometimes — for whatever electronic reason I won't try to understand right now — one of the tasks makes it through your CPU faster than a previous one, thus overtaking it and printing its number out first.

Pretty cool, huh?

Looking forward to exploring more, and maybe using Gleam for when I don't need to be quite as close to the metal as Rust gets me.