Skeleton screens, but fast
Tags: performance, html, css, marko
Published: 2021-04-28 17:18:05

---
title: Skeleton screens, but fast
published: true
description: Why use loading indicators if the code for them makes the load slower?
tags: performance,html,css,marko
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/bfwqveq00p0js59qqgb8.jpg
---

Here’s a fun HTTP+HTML+CSS technique for skeleton screens that works in almost* any stack, and some small but important details we need to do right by it.

Most importantly, it involves **no client-side JavaScript**, because adding JS to make a website feel faster usually is counter-productive. In fact, Zach Leatherman inspired this post by saying:

{% twitter 1352314419343052800 %}

{% twitter 1352317811285237761 %}

<small>\* The newer isomorphic ones like React struggle mightily to stream over HTTP, with one exception — I’ll get to it later.</small>

## Skeleton screens?

Or indicators/placeholders/whatever. The “new” design hotness for when computers aren’t ready to show you something: skeleton screens!

<figure>

![Screens depicting skeletons?](https://dev-to-uploads.s3.amazonaws.com/i/i21eavef7r8q6ml10nm3.jpg)

<figcaption>No, not nearly that entertaining.</figcaption>
</figure>

Instead of a spinner or progress bar, show something _shaped_ like the eventual content — it orients the user faster, hints at what to expect, and avoids the page jumping around as it loads:

<figure>

![The Polar app used a skeleton screen for the name, image, and profile info of its user details page.](https://dev-to-uploads.s3.amazonaws.com/i/injgls3xysadxii4p5r0.png)

<figcaption><a href="https://www.lukew.com/ff/entry.asp?1797">The loading process of Polar</a>, one of the first apps to popularize the concept of skeleton screens.</figcaption>
</figure>

## Demo

We can’t avoid the time it takes to call a search results API — we can cache its responses, but how can you cache all possible search queries ahead of time?

Here’s what these search skeletons look like with an artificial search API response delay of 5 seconds:

{% vimeo 503580605 %}

And here’s some code for how they work:

```html
<SiteHead />

<h1>Search for “${searchQuery}”</h1>

<div.SearchSkeletons>
  <await(searchResultsFetch)> <!-- stalls the HTML stream until the API returns search results -->
    <@then|result|>
      <for|product| of=result.products>
        <ProductCard product=product />
      </for>
    </@then>
  </await>
</div>
```

This is that “one exception” I mentioned earlier. [Marko](https://markojs.com/) is a JS component framework similar to React, but is actually good at server-side rendering — in particular, built-in support for HTTP streaming. (And last I checked, it’s nearly the only thing in Node that does. RIP Dust)

If you’re more familiar with other languages/frameworks, here’s how they accomplish something similar to [Marko’s `<await>`](https://markojs.com/docs/core-tags/#await):

<dl>
<dt><b>PHP</b><dd><a href="https://www.php.net/manual/en/function.flush.php"><code>flush()</code></a> and <a href="https://www.php.net/manual/en/function.ob-flush.php"><code>ob_flush()</code></a><br/><br/>

<dt><b>Ruby on Rails</b><dd><a href="https://api.rubyonrails.org/classes/ActionController/Streaming.html"><code>ActionController::Streaming</code></a><br/><br/>

<dt><b>Spring</b><dd><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.html"><code>StreamingResponseBody</code></a><br/><br/>

<dt><b>ASP.net</b></dt><dd>I recommend searching for ASP’s <code>.BufferOutput</code> and <code>.Flush()</code> yourself, because it’ll also turn up results <a href="https://blog.maartenballiauw.be/post/2018/06/14/how-http-chunked-encoding-was-killing-a-request.html">warning about possible footguns</a>.<br/><br/>

<dt><b>Django</b><dd><a href="https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.StreamingHttpResponse">There’s a <code>StreamingResponseBody</code></a>, but Django really doesn’t care for it. You may need to get creative.<br/><br/>

<dt><b>Others not listed here</b><dd>Try searching for them plus “http stream” or “chunked transfer-encoding”.<br/><br/>
</dl>

By not waiting on search results before sending HTML, browsers get a head start downloading assets, booting JS, calculating styles, and showing the `<SiteHeader>` and `<h1>`.

```css
.SearchSkeletons:empty {
  height: 110vh; /* Skeletons take up at least the full viewport */
  background-image: …/* Assume this is an image of the skeletons for now */…;
}

.SearchSkeletons::before { /* This is the faded white bar that scrubs across the skeletons */
  content: "";
  position: absolute;
  height: 100%;
  width: 3rem;
  background: linear-gradient(rgba(white, 0) 10%, rgba(white, 0.5), …);
  animation: shimmer 2.5s linear infinite;
}

@keyframes shimmer {
  0% { transform: translateX(-100%) }
  100% { transform: translateX(100vw) }
}
```

[The `:empty` pseudo-class](https://developer.mozilla.org/en-US/docs/Web/CSS/:empty) is the key:

1. While waiting for the search API, the opening `<div class="SearchSkeletons">` is streamed to browsers, without children or a closing tag.

2. `:empty` only selects elements without children, such as the aforementioned `<div>`.

3. As soon as the HTML resumes streaming and fills `.SearchSkeletons` with results, `:empty` no longer applies.

4. The skeleton styles disappear at the same time the `<ProductCard>` components display, reanimating the product skeletons into real products.

A nice thing about this approach is that if the search endpoint responds quickly, `:empty` _never_ matches and browsers waste no resources styling or displaying the product skeletons.

## Avoiding style recalculation

Do we need `:empty`? Couldn’t this also work?

```html
<SiteHead />

<h1>Search for “${searchQuery}”</h1>

<style>
  .SearchSkeletons {
    …
  }
</style>
<div.SearchSkeletons></div>

<await(searchResultsFetch)>
  <@then|result|>
    <style>
      .SearchSkeletons { display: none }
    </style>

    <for|product| of=result.products>
      <ProductCard product=product />
    </for>
  </@then>
</await>
```

Yes, that does work. But it’s slower: appending new CSS to a document triggers [“style recalc”](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), where browsers update their selector buckets, invalidate and re-match elements, etc.

We can’t avoid browsers performing [_reflow_](https://developers.google.com/speed/docs/insights/browser-reflow), as that always happens when new HTML streams in. But by avoiding additional style recalc:

- Browsers show the new HTML sooner
- User interaction doesn’t hitch as much
- There’s more CPU time left over to run JavaScript

Using `:empty` vs. additional `<style>` elements is a subtle decision, but it impacts user experience just the same.

Hopefully, this illustrates why a strong understanding of HTML and CSS is important for making a site fast.

## Hardware-accelerated animation or bust

And if _that_ didn’t illustrate why a strong understanding of HTML and CSS is important for making a site fast, **this sure as hell will**.

A predefined `@keyframes` that only changes the `transform` property is one way to [ensure that an animation is hardware-accelerated on the GPU](https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/). That means it frees up the CPU for all the other responsibilities of the main thread: parsing, JavaScript, user interaction, reflow…

Skeleton animations that run on the main thread have a _raft_ of complications:

- The shimmer animation will hiccup and stall whenever JavaScript executes, the document reflows, style recalculates, or JSON is parsed.
- The time the CPU spends running the animation makes the above tasks take longer.
- **The loading indicator delays the content it’s a placeholder for!**

At my job, I changed a similar loading animation from using `background-position` to `transform`. The page FPS went from 49 to 55 on a powerful developer MacBook — imagine how much more on mobile!

## But wait, there’s more!

Remember this from the earlier code sample?

```css
background-image: …/* Assume this is an image of the skeletons for now */…;
```

The TL;DR is the background image is made of CSS gradients and so that the skeleton is shown ASAP. It makes no sense to have your loading indicator wait on an HTTP request, does it?

I implemented the background images with Sass variables to prevent the skeletons from drifting out of sync with the product cards if any changes were made. For example, if I tweaked the padding of the actual product cards, the following code would also update the spacing of the skeletons:

```scss
$skeleton-color: #dfe1e1;

$card-padding: 0.5rem;
$card-height: 8.125rem;

$img-height: 70%;
$img-width: 45%;
$img-position: right $card-padding top 0;
$img-skeleton: linear-gradient(
  transparent 15%,
  $skeleton-color 15%,
  $skeleton-color ($img-height + 15%),
  transparent ($img-height + 15%)
);

$name-line-size: 0.844rem;
$name-line-1-width: 13ch;
$name-line-1-offset: $card-padding;
$name-line-1-position: $card-padding 0;
$name-line-1-skeleton: linear-gradient(
  transparent $name-line-1-offset,
  $skeleton-color $name-line-1-offset,
  $skeleton-color ($name-line-1-offset + $name-line-size),
  transparent ($name-line-1-offset + $name-line-size)
);
$name-line-2-width: 10ch;
$name-line-2-offset: $card-padding + $name-line-size + 0.2rem;
$name-line-2-position: $card-padding 0;
$name-line-2-skeleton: linear-gradient(
  transparent $name-line-2-offset,
  $skeleton-color $name-line-2-offset,
  $skeleton-color ($name-line-2-offset + $name-line-size),
  transparent ($name-line-2-offset + $name-line-size)
);

$price-height: 1.5rem;
$price-width: 4ch;
$price-offset: $name-line-2-offset + 1.3rem;
$price-position: $card-padding 0;
$price-skeleton: linear-gradient(
  transparent $price-offset,
  $skeleton-color $price-offset,
  $skeleton-color ($price-offset + $price-height),
  transparent ($price-offset + $price-height)
);

.SearchSkeletons:empty {
  background-repeat: repeat-y;
  background-image:
    $img-skeleton,
    $name-line-1-skeleton,
    $name-line-2-skeleton,
    $price-skeleton
  ;

  background-size:
    $img-width $card-height,
    $name-line-1-width $card-height,
    $name-line-2-width $card-height,
    $price-width $card-height
  ;

  background-position:
    $img-position,
    $name-line-1-position,
    $name-line-2-position,
    $price-position
  ;
}

@media (min-width: 30rem) {
  .SearchSkeletons {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(20rem, 0.75fr));
    grid-gap: 1rem 2rem;
    justify-content: center;

    &:empty { /* TODO show how to use `background-repeat-x: round` to make skeletons responsive */
      height: auto;
      background: none;
    }
  }
}
```

Here’s what that Sass compiles to:

```css
.SearchSkeletons:empty {
  background-repeat: repeat-y;
  background-image:
    linear-gradient(transparent 15%, #dfe1e1 0, #dfe1e1 85%, transparent 0),
    linear-gradient(transparent .5rem, #dfe1e1 0, #dfe1e1 1.344rem, transparent 0),
    linear-gradient(transparent 1.544rem, #dfe1e1 0, #dfe1e1 2.388rem, transparent 0),
    linear-gradient(transparent 2.844rem, #dfe1e1 0, #dfe1e1 4.344rem, transparent 0);
  background-size:
    45% 8.125rem,
    13ch 8.125rem,
    10ch 8.125rem,
    4ch 8.125rem;
  background-position:
    right .5rem top 0,
    .5rem 0,
    .5rem 0,
    .5rem 0;
}
```

I was going to finish this post with how to make these mobile-first styles responsive using `background-repeat`, but it was making me put off publishing this altogether, _and that’s terrible_. If you’re interested, let me know and I’ll write a followup.

## Update

I haven’t written that followup yet, but [Jason “scurker” shared that the AXE browser extension does this too](https://twitter.com/scurker/status/1387546619046989829), and was able to share some code for how they do repeating skeleton backgrounds:

{% gist https://gist.github.com/scurker/a65b5bedeb9b31e80e67b8d04daabe82 %}