There are quite a lot of frontend developers in our audience at Snipcart, and many of them dig performance-enhancing activities. So to live up to our promise of offering a fast & custom shopping cart for developers, we recently reviewed our whole customization process. While doing so, we kept a code journal and discussed how we used Gulp.js to optimize our frontend workflow. And it was our most popular post of the year!

In the last few weeks, we started to look more seriously into an underserved area of our ecosystem: website performance. I'm not afraid to say it; our site has its fair share of performance problems. We didn't put this crucial aspect front and center when we launched it. This was a mistake. I see it now: it affects both our overall user experience (dwell time, bounce and conversion rates, etc.) and, of course, our SEO. But we were a smaller, busier team at the time.

Recently, using tools like PageSpeed Insights, Yellow Lab & YSlow, we started to put together a painful diagnosis of our site. We started with quick wins, such as using minified versions of our CSS, compressing stuff using gzip, and replacing JavaScript animations with CSS animations on our homepage.

Today, I'm going to discuss the latter with you.

While a few JS animations are far from being a top performance priority, turning them into CSS animations can still offer small wins such as fewer DOM manipulations and JS files load. Plus, they're technically cool & fun to do!

In this post, I'm going to show you how I've re-created the animations on our homepage using CSS. I'll also provide a few tips for developers who would like to do something similar along the way!

Dat CSS

Like any passionate frontend developer, I love getting my hands dirty with JavaScript. However, when I first started learning the ways of the web, I can remember being utterly captivated by some of the "pure CSS" animations I would come across.

Despite CSS and I having our differences at the time, I invested a majority of my time perfecting my stylesheet game. It soon became clear that what I was animating with jQuery could be done through CSS, yielding smoother results without the bulk of external libraries.

It is truly fascinating how far the language has come with the third spec. There are multiple advantages when using CSS for these types of dynamic features. Chief amongst them is the fact that we're relying on something native to all browsers. This not only makes it a lightweight solution, but also an efficient one. Vendors have poured massive resources into streamlining their CSS parsers so it goes to reason that, used appropriately, it will be easier to compute than JavaScript being run through whichever framework takes your fancy.

Not so cutting Edge

The dev who built the Snipcart homepage wasn't around when I was asked if I could replace the JS animations with CSS. So the first order of business was to take a look under the hood and figure it out on my own.

Running the show was Adobe Edge Animate CC, a development tool for creating vector based browser animations. Turns out Adobe ceased active development of the software in November 2015 and is unavailable as part of Creative Cloud.

With already reason enough to make the swap, I took a look at the network dev tools to see what we were loading in.

  • The Edge JavaScript library at 102kb
  • Five separate scripts, one for each animation — a combined 31kb
  • 39 individual SVG files loaded as images weighing in at just about 35kb

I dug around a little more and found that the five animation scripts, as well as the SVG images, were being dynamically called and injected after initial page load.

So we're talking nearly double jQuery's weight, countless DOM manipulations and a myriad of unnecessary style properties for the browser to consider. Let's get to work then!

In with the new

I went for the simplest possible approach in recreating the animations. On one screen I pulled up the Snipcart homepage, and on the other CodePen. Watch the original animation — recreate.

With that said, let's take a look at the process. You can check out the code/result in this pen.

The layers

I already had access to the independent SVG files I needed. Given the fact that browsers can interpret these XML-based vector graphics inline with the HTML, there's no need to make extra requests to the server.

Using Gulp SVG Sprite and a quick Gulp task, I minified all the files into a single spritesheet. This only shaved off about 1kb, but eliminated 39 requests to the server!

The gulp module produces a single SVG with <symbol> elements. Each symbol's ID is based on the original vector's filename. Simply inline the spritesheet at the top of the page and call the symbols you need, like so:

  <svg><use xlink:href="#symbol_ID"/></svg>

In your HTML, group layers that move together and set your elements in the right order to avoid manipulating their
z-index unnecessarily. It's always best to let your markup work for you.

The keyframes

Strategy is key when getting into complex animations through CSS. Here are a few things to keep in mind:

  • Keep things simple. Use clear classnames and animation names. It's not worth saving a few characters here and a couple of lines there if it obfuscates your code.
  • Stick to CSS transforms when possible. The two main reasons are much smoother results and fewer browser repaints. Paul Irish wrote a great article on the subject and certainly explains it much better than I ever could.
  • Leverage "hardware acceleration" by setting backface-visibility: hidden; to the animation's parent element. The browser will think you're going to do some 3D transforms and takes measures to help keep things at a silky smooth 60fps.
  • JavaScript should only be called upon to trigger the animations.

Set the animation's parent element's animation-play-state to paused and all its children to inherit. This way we only need to toggle the parent's play-state when it's in the viewport.

Now we get to the fun part, the CSS animations and keyframes. Timing is everything here, so expect a lot of fiddling to get things right. Here's a slightly modified example taken from the first animation's "C5" layer, omitting non-relevant styles.

  <div class="anim1">
    <svg class="anim1__layer anim1__layer--base"><use xlink:href="#anim1__base"/></svg>
    <svg class="anim1__layer anim1__layer--c5"><use xlink:href="#anim1__c5"/></svg>
</div>

  .anim1 {
    backface-visibility: hidden;
    animation-play-state: paused;

    &__layer {
        width: 100%;
        position: absolute;
        left: 0;
        top: 25%;
        animation-timing-function: cubic-bezier(.4,.25,.3,1);
        animation-fill-mode: both;
        animation-play-state: inherit;

        &--base {
            transform: translateY(50%);
            opacity: .5;
        }

        &--c5 {
            animation-name: anim1__c5;
            animation-duration: 5s;
            animation-delay: 1s;
        }
  }
}

@keyframes anim1__c5 {
    0% {
        transform: translateY(-50%);
        opacity: 0;
    }
    40%, 60% {
        transform: translateY(0);
        opacity: 1;
    }
    100% {
        transform: translateY(50%);
        opacity: 0;
    }
}

In the above snippet, I've set the --c5 layer's animation-duration to 5s. In the keyframes, I tell it to fade in and move down half its own height over the first 40% of the animation's duration, so 2 seconds. Then I keep it in that state until 60%, 3 seconds in this case. Finally, it fades out and moves down again over the last 2 seconds.

The trigger

JavaScript is only used to trigger the animations when they're visible. If you're not careful, especially when listening on scroll events, this can still be a performance killer. Here are some tips to avoid scroll bottlenecks:

  • Keep the DOM query outside the handler function so you don't repeat it.
  • Debounce or throttle your handler. In my case I used the requestAnimationFrame() function.
  • Avoid "playing" an animation that has already run.
  • Remove your listener from the scroll event once all the animations have run.

For us, it only took 25 lines of native JS to get the job done.

Conclusion

The whole process took me a solid 15 hours. So this begs the question — was it worth it? I'll let the numbers speak for themselves.

Putting JavaScript on a diet really paid off. We shaved 132.5kB off the original 133kB, a massive 99.6% economy! We dropped 11 server requests, cut down 41% on queries without result, reduced DOM access by 25.4% and DOM access on scroll by 17%.

As for page load time, the metric that matters most? You'll be glad to know you can now access the homepage 3.68 seconds faster!

If you ask me, it was well worth it. I hope this motivates you to do your part in tending to the web's obesity epidemic.

As Richard Simmons once said:

One day I may be meeting you and hearing how you've changed your life by saying, "Farewell to Fat".

I hope you enjoyed reading about these CSS animations as much as I have enjoyed creating them. They're live on our homepage, so feel free to have a look!

In the near future, we intend to focus on the following optimizations:

  • Reduce the number of HTTP requests and DNS resolve our site is making
  • Cache static resources to diminish load times for returning users
  • Compress all images on site to reduce load time

We'll keep everyone via our newsletter about how this all goes!


If you liked this post and found it valuable in any way, please, take a second to share it on Twitter. I'd also love to know your thoughts on CSS animations or web perf in general in the comments!


6,527 1 52