Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Rob Dudley
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Rob Dudley@robdudley )

Delaying Loading Indicators Using CSS Animations In Angular 9.0.0-next.14

By Ben Nadel on

I don't really know much about React. And, I certainly know nothing about the upcoming "Suspense" feature. But, from what I've heard various people say on various podcasts, it seems that one of the features provided by Suspense is the ability to delay the rendering of "Loading Indicators" in an attempt to create a better user experience (UX). I am sure there is a lot more to it than what I've picked up; but, based on this superficial understanding, I wanted to see if I could create a similar type of delay using CSS animations in Angular 9.0.0-next.14.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

So, from what I think I've heard people discuss, showing a loading indicator can sometimes increase the perception of latency if the latency is relatively low. And, apparently, in low-latency contexts, the user interface will seem faster if you omit the loading indicator altogether and just show a "blank screen" before the content loads.

Honestly, I am not sure how I feel about this. But, I'm not opposed to trying it out for myself. And, it seems that this type of behavior can be easily accomplished with a simple CSS Animation delay. The idea here being that the calling context won't manage the delay - the calling context simply shows and hides the loading indicator the same it would traditionally. The loading indicator would then manage the delay internally using the animatable CSS property, opacity.

Before we look at the loader, though, let's look at the App component. To experiment with this idea, the App component will toggle the display of a "content area" that is delayed by a simulated network request. While the simulated network request is pending, the App component will show a loading indicator, passing-in a [delay] property.

There are several toggle() calls, each of which uses a different [delay] value for the loading indicator. This will help us get a sense of how different delays affect the perception of latency:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "app-root",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<p>
			Toggle Container with delay:
			<a (click)="toggle( 0 )" class="toggle">0ms</a>,
			<a (click)="toggle( 300 )" class="toggle">300ms</a>,
			<a (click)="toggle( 500 )" class="toggle">500ms</a>,
			<a (click)="toggle( 1000 )" class="toggle">1,000ms</a>
		</p>

		<section *ngIf="isShowingContainer">

			<!--
				The [delay] property determines the number of MS to wait before the
				loading indicator is renderered. By pushing this logic into the loader,
				it keeps the calling logic simple and binary (ie, show / don't show). The
				underlying THEORY here is that the precense of the loading indicator can
				increase the perceived delay when the latency is relatively low.
			-->
			<app-loader
				*ngIf="isLoading"
				[delay]="delay"
				class="loader">
			</app-loader>

			<div *ngIf="( ! isLoading )">
				From the corner of the gym where the BIG men train,<br />
				Through a cloud of chalk and the midst of pain<br />
				Where the big iron rides high and threatens lives,<br />
				Where the noise is made with big forty-fives,<br />
				A deep voice bellowed as he wrapped his knees,<br />
				A very big man with legs like trees.<br />
				Laughing as he snatched another plate from the stack<br />
				Chalking his hands and monstrous back,<br />
				said, "Boy, stop lying and don't say you've forgotten,<br />
				The trouble with you is you ain't been SQUATTIN'."<br />
				&mdash;DALE CLARK, 1983
			</div>

		</section>
	`
})
export class AppComponent {

	public delay: number;
	public isLoading: boolean;
	public isShowingContainer: boolean;

	// I initialize the app component.
	constructor() {

		this.delay = 0;
		this.isLoading = false;
		this.isShowingContainer = false;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I toggle the visibility of the data container, which triggers a "network" request
	// using the given delay for the loading indicator.
	public toggle( newDelay: number ) : void {

		this.delay = newDelay;

		// Toggle the container closed.
		if ( this.isShowingContainer ) {

			this.isShowingContainer = false;
			this.isLoading = false;

		// Toggle the container opened.
		} else {

			this.isShowingContainer = true;
			this.isLoading = true;

			this.getData().then(
				() => {

					this.isLoading = false;

				}
			);

		}

	}

	// ---
	// PRIVATE METHODS.
	// ---

	// I simulate a network request with a random amount of latency.
	private getData( maxLatency: number = 1000 ) : Promise<void> {

		var promise = new Promise<void>(
			( resolve ) => {

				var latency = Math.floor( Math.random() * maxLatency );

				console.log( "Data Fetch Latency:", latency, "ms" );
				setTimeout( resolve, latency );
				
			}
		);

		return( promise );

	}

}

As you can see, the presence of the loading indicator is controlled by a simple *ngIf="isLoading" directive. The App component only cares about whether or not the loading indicator is present. It defers all of the "delay" implementation to the loading indicator itself.

So, let's look at the loading indicator component. The component logic is minimal, just showing the text, Loading...:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "app-loader",
	inputs: [ "delay" ],
	host: {
		"[style.animation-delay.ms]": "delay"
	},
	styleUrls: [ "./loader.component.less" ],
	template:
	`
		<div class="indicator">
			Loading....
		</div>
	`
})
export class LoaderComponent {

	public delay: number = 0;

}

For the implementation of this loader, we're going to use CSS animation to delay the rendering of the loader on the screen. Well, to be clear, the loader will be there immediately; however, it will be transparent with an opacity of 0.

Notice that we have the following host binding:

"[style.animation-delay.ms]": "delay"

In this implementation, the [delay] input is being parled into a Style property, animation-delay. This is then used in the LESS file to determine when the opacity property gets flipped from 0 to 1:

:host {
	animation-delay: 0ms ; // This will be overridden in the HTML template.
	animation-duration: 250ms ;
	animation-fill-mode: both ;
	animation-name: loader-component-keyframes ;
	display: flex ;
}

// The opacity property is going to be used to drive the visibility of the loader. It
// will start out as transparent (ie, not visible); and then, the animation-delay
// property defined in the component template (in conjunction with the keyframes) will
// determine how long to wait before the indicator is rendered for the user.
// --
// NOTE: Using animation-fill-mode of "both" causes the 0% state to be applied to the
// component when it is first rendered.
@keyframes loader-component-keyframes {
	0% {
		opacity: 0.0 ;
	}

	100% {
		opacity: 1.0 ;
	}
}

.indicator {
	flex: 0 0 auto ;
	margin: auto auto auto auto ; // Center vertically and horizontally.
}

In this case, we're using the animation-fill-mode value of both to indicate that the 0% keyframe should style the component prior to the animation; and, that the 100% keyframe should style the component following the animation. Which means, from the initial rendering though to the animation-delay, the loading indicator will be transparent. And then, once the animation kicks off, the loading indicator will become opaque.

Now, when we run this Angular application in the browser and trying toggling the content area using different delay values, we get the following output:

Various delays affect perceived performen when showing a loading indicator in Angular 9.0.0-next.14.

This is not an easy thing to see in an animated GIF. I suggest trying it out for yourself. That said, there may be a bit of sweet-spot, maybe somewhere in the sub-300ms delay range? But, again, I'm not sure how I feel about it. And, I would likely have to see this in action in a robust Angular application before I could really get a sense of whether or not it is worth it. Of course, by encapsulating the delay implementation within the loading component itself, this is something that could be added, removed, or modified in a cross-cutting way once the application has been built.

As a reminder, I know nothing about React Suspense. So, please forgive me if any of understandings here are, in fact, misunderstandings. That said, I am intrigued by the idea of delaying the rendering of a loading indicator in an attempt to decrease the perceived latency by the user. And, I love how easy it is to implement this kind of delay using CSS animations, host properties, and input bindings in Angular 9.0.0-next.14.



Reader Comments

Yes. Loading indicators have always been a tricky one for me.

One of my approaches is to build the indicator dynamically. Just before an AJAX call, I create a DOM element that represents the indicator and add it to the parent container.

Once the AJAX call has completed and I have content from the server,
I just load the content into the parent container, thus deleting the indicator completely.

In this way, I can be sure that the indicator is running up until the time that new content appears on the screen.

Like:

<InvalidTag>

$("#get-content").on("click",function(){
    var parent = $("parent");
    var indicator = '<div class="indicator"></div>';
    var error = '<div class="error">An error has occurred. Could not load content</div>';
    parent.append(indicator);
    var jqxhr = $.ajax({
        url: "api/content"
    })
    .done(function(data) {
        parent.html(data);
    })
    .fail(function() {
        parent.html(error);
    });
});

</script>

<div id="parent"></div>

Of course the drawback to this approach is that there is a possibility that the dynamically created indicator never gets deleted, if the AJAX request fails.

I try and mitigate this by creating a delete routine inside the AJAX fail handler! But sometimes this handler is never fired. I heard that jQuery AJAX has a timeout feature as well, but I have found that this isn't too reliable.

I'm really not sure what the delay approach adds to the loading routine. But, I'm sure people far smarter than me have been thinking about this carefully, so I will bow to their better judgement, on this matter.

Reply to this Comment

@Charles,

I think we're doing very similar things. I'm just using the ngIf directive to create and inject the loading spinner into the DOM whereas you're using jQuery to do pretty much the same.

Re: what the delay "adds"? Yeah, I'm not entirely sure. I think there is a belief that a really short flash of a loading spinner is a worse experience when compared to no flash at all and then the loading of the content.

To be honest, I'm not sure how strongly I feel. The nice thing about building it into the app-loader component is that I can always change my mind later on, since the delay logic is built into the component. We'll see.

Reply to this Comment

OK. I get you now.

So, the spinner may not kick in at all, if there is a delay.
In my/your example, the AJAX content might have overwritten the animation content by the time the delay has expired.

Yes. I quite like this now. So, in the CSS, you can add a delay, and if the AJAX content loads quickly, then the user is none the wiser, that there was ever a loader, at all.

Maybe, I will implement this from now on. It's a pretty easy update to add to both future & legacy projects alike...

And, I guess we can control the animation delay time value from a global variable, so it is easy to adjust.

I reckon your 1000ms delay is the one I will go for...

Reply to this Comment

@Charles,

Yeah, that's the idea. The loader is always "there" during the AJAX-load, it's just not always visible to the user because of the opacity and the animation-delay CSS properties.

To be clear, I am not sure how strongly I feel that this makes an actual user-experience difference -- I am just exploring the ideas that I hear people talk about when it comes to React Suspense. Though, I think there is probably some merit to the idea.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.