Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 28, 2021 05:18 pm GMT

Skeleton screens, but fast

Heres 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:

* The newer isomorphic ones like React struggle mightily to stream over HTTP, with one exception Ill get to it later.

Skeleton screens?

Or indicators/placeholders/whatever. The new design hotness for when computers arent ready to show you something: skeleton screens!

Screens depicting skeletons?

No, not nearly that entertaining.

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:

The Polar app used a skeleton screen for the name, image, and profile info of its user details page.

The loading process of Polar, one of the first apps to popularize the concept of skeleton screens.

Demo

We cant 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?

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

And heres some code for how they work:

<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 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, its nearly the only thing in Node that does. RIP Dust)

If youre more familiar with other languages/frameworks, heres how they accomplish something similar to Markos <await>:

PHP
flush() and ob_flush()

Ruby on Rails
ActionController::Streaming

Spring
StreamingResponseBody

ASP.net
I recommend searching for ASPs .BufferOutput and .Flush() yourself, because itll also turn up results warning about possible footguns.

Django
Theres a StreamingResponseBody, but Django really doesnt care for it. You may need to get creative.

Others not listed here
Try searching for them plus http stream or chunked transfer-encoding.

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

.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(100%) }}

The :empty pseudo-class 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? Couldnt this also work?

<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 its slower: appending new CSS to a document triggers style recalc, where browsers update their selector buckets, invalidate and re-match elements, etc.

We cant avoid browsers performing reflow, as that always happens when new HTML streams in. But by avoiding additional style recalc:

  • Browsers show the new HTML sooner
  • User interaction doesnt hitch as much
  • Theres 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 didnt 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. 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 its 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, theres more!

Remember this from the earlier code sample?

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:

$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;    }  }}

Heres what that Sass compiles to:

.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 thats terrible. If youre interested, let me know and Ill write a followup.


Original Link: https://dev.to/tigt/skeleton-screens-but-fast-48f1

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To