41

A Brief History of Flickering Spinners

 5 years ago
source link: https://www.tuicool.com/articles/hit/mm6VFbq
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

A phenomenon we are seeing more of lately is flickering spinners. A brief splash of something spinning flashing before your eyes. Somehow you know it’s a spinner but you cannot make out the details. What just happened?

We do have fast internet nowadays. Back in the days, it was fine, whenever loading something over the network, to show a spinner. Things took longer time. Nowadays, things are a bit more complicated. Not only do we have faster internet connections we also load more data. That data comes in more requests and often in smaller chunks. It’s not uncommon today to have several elements in a page gradually and separately loaded.

The rise of asynchronous programming

One reason behind this development is the change we’ve seen in asynchronous programming. Asynchronous programming is a lot easier than it used to be. Most modern languages have good support for loading data on the fly. Modern JavaScript has incorporated Promises and with ES7 comes the async and await keywords. With the async/await keywords one can easily fetch data and process it when needed. This means that we need to think a step further about how we show users that data is loading. The good old show a spinner doesn’t work out of the box anymore. Here’s what, in pseudo code, traditional loading with a spinner might look like:

function loadNetworkData() {
  // Show spinner
  showSpinner();
return fetch(...).then(() => {
    // When network returns hide the spinner
    hideSpinner());
    ...
  }
};

Progressively enhancing web pages

Hand in hand with the development in asynchronous programming comes another trend. Progressive Enhancement. This technique lets users consume content before pages are completely loaded. The idea lets us, as many things in the modern world, consume more content faster. Progressive Enhancement heavily relies on gradually loading data. It encompasses concepts such as Lazy loading, Progressive loading and Above the fold. These all, in one way or another, involve loading data in stages. Sometimes visualized with a spinner. In other cases, other types of indicators are more appropriate. Either way, the idea is to let our user know that page is progressively enhanced. Lazy loading with fade-in animations is another popular technique. It gently shows the user that data is gradually loaded in the background.

The UX killer

Animations sure can be helpful. The right kind of animation will help and guide your users throughout the UI. But, if they’re too quick they will more likely distract the user. Flashing animations lead to cognitive overload. Or, in other words, the animations become destructive. In the end, we’ll end up with a worse user experience. As this is 2018, we all know bad UX is killer. It does more harm than good. Convergence and churn are two, nowadays, very familiar terms. Especially to anybody involved in building software products, apps and services. Not to mention to people working with growth.

The spinner, do we actually need it?

The first thing we have to ask ourselves: Do we need a spinner at all? In some cases, we can use more subtle loading indicator UX. Why not the three dots … or something more subtle than a spinner. Sometimes we don’t even need an indicator at all. We don’t always have to have a spinner.

In some cases, though, the spinner still makes a lot of sense. It might be a mobile app loading data over cellular networks. It could be an application loading bigger chunks of data. Some scenarios need a lot of processing before returning the data. For cases like these, we must make sure that the user has a pleasant experience. Pleasant here means something that uninitiated users can detect, grasp, process and understand. The spinner is there to reassure the user that something is happening. It, by definition, needs a longer break. Relax, sit back, we’re working on it kind of thing. These are not 200 ms moments.

Gracefully loading components

To deal with this problem I usually use, what I refer to as, the wait-at-least pattern. Here is what it looks like in code:

function waitAtLeast(time, promise) {
  const promiseTimeout = new Promise((resolve) => {
    setTimeout(resolve, time);
  });
  const promiseCombined = Promise.all([promise, promiseTimeout]);
return promiseCombined.then((values) => values[0]);
};

And in a modified ES7 version

async function waitAtLeast(time, promise) {
  const promiseTimeout = ...
  const promiseCombined = Promise.all(...);
const values = await promiseCombined;
  return values[0];
};

If you’re not used to promises or to asynchronous JavaScript for that matter the above might be a bit hard to digest. The function does two things 1. It creates a new promise that resolves when a timer has fired and 2. It waits for both the provided promise, the network request, and the timer promises to resolve. When they both eventually do resolve the function finally returns the value. I.e the data from the network request. Still hard to grasp? Ok, let’s have a look at an example. Let’s say, for the sake of an argument, that the timer is set to one second. Then there is two possible scenarios

  1. The network returns the data in less than a second. The combined promise will resolve when the timer fires after 1 second. As both promises need to complete, the function will first return after a second.
  2. The network request takes exactly or more than 1s. The combined promise will then resolve when the network request returns. As the timer already expired while waiting, the response is immediately available.

Here is how you use it. What previously looked something like this:

fetch(...) // or other async function returning a promise
  .then(...)
  .catch(...)
  .finally(...);

Now becomes

const promise = fetch(...);
waitAtLeast(800, promise) // wait at least 800ms before handling the response
  .then(...)
  .catch(...)
  .finally(...);

Demo time

Below is a demo that illustrates how to pattern works.

The demo above comes with a slider in the bottom. This slider lets you try out different network latency. With different latency, you can see the effect of turning on/off the wait-at-least pattern. It becomes especially interesting to see the effect at low latency. Here is where the technique becomes notably useful.

Designing the delay

So for how long should we keep our users waiting? Well, the Material Design UX guidelines give us a good idea of how to design the delay:

When elements change their state or position, the duration of the animation should be slow enough to give users the possibility to notice the change, but at the same time quick enough not to cause waiting.

The wait-at-least pattern in single page application frameworks

What about flickering spinners/graphics in SPA frameworks. Like e.g. React. Can we reuse the same pattern in React? Sure, it’s applicable here as well. Let’s say we fetch data in the componentDidMount callback. Then to gracefully show the change we can hold off the update to the component’s state. In pseudo code:

componentDidMount() {
  const promise = fetch(...);
waitAtLeast(500, promise)
    .then(data => this.setState({ data }));
}

Since React components have a life cycle we must also remember to support cancelling the waitAtLeast timer. That is if the component is unmounted. This we can do in the componentwillunmount callback.

Wrapping up

  • Flickering is bad UX
  • Loading data should be a pleasant experience when data is progressively added to the page
  • The wait-at-least pattern is a good way to ensure pleasant transitions, especially for spinners.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK