This page details implementation techniques and common loading scenarios for eBay Skin's skeleton module.
NOTE: This page demonstrates both good and bad uses of skeletons; please be sure to read and understand carefully to avoid replicating an anti-pattern!
Techniques
Skeletons can be used as placeholders for content (i.e. reserving physical space in the page) using either a pure CSS approach, or a DOM manipulation approach with JavaScript.
Technique 1: Pure CSS
This technique leverages the CSS :empty pseudo-class, as documented in the article, "Skeleton screens, but fast" by Taylor Hunt. This approach is highly performant, especially on devices with limited CPU resources.
If an element is empty, then its :empty selector matches, and the associated styles (i.e. the skeleton graphics) are applied. When the content is rendered, :empty no longer matches, and the styles (our skeleton graphics) are removed. Marvellous!
The HTML would look like this:
The CSS selector like so:
NOTE: The :empty pseudo-class considers elements with whitespace as not empty!
The downside is that our skeleton classes cannot be leveraged (in theory, LESS mixins could be provided) and composite skeletons (e.g. items tiles) may require some non-trivial use of CSS linear gradients for certain skeletons.
Technique 2: JavaScript
With JavaScript, we gain more flexibility and control over when and how the skeleton placeholder is replaced with the actual content. We can either remove the skeleton node from the DOM, as shown below.
Or we can simply hide the skeleton, using the hidden property.
Notice that JavaScript also gives us the ability to add and remove ARIA roles, properties and states.
Page Loading Scenarios
For the purposes of keeping examples simple, we use a basic blog page template for conveying common loading techniques. In reality, blogs by their nature should be highly performant (i.e. fetching cached, static content) and would not require any kind of asyncronous rendering or skeleton placeholders. Please keep this in mind and think very carefully about whether your type of page and architecture actually requires the technical overhead of skeletons or not.
We consider 4 main scenarios for how a full page may be constructed:
- Buffered (default)
- Client-Side Rendered (SPA)
- In-Order Streaming (Progressive Rendering)
- Out of Order Streaming
Of course this is not the whole story! After the page has been loaded, users may interact with components that trigger partial page updates, in which case skeleton placeholders may also be utilized.
Scenario 1: Buffered Rendering
With buffered rendering (the default manner in which web pages are loaded) the client waits for the entire HTML to be sent by the server before rendering. Therefore, in this scenario, there is no opportunity to display skeleton placeholders, because the content arrives all at once (one caveat would be a SPA, which we cover in the next section).
The Empty Screen
This example simulates the delay (set to 3 seconds) and empty screen while the client waits for the entire HTML payload from the server.
For many types of web site, so long as the delay is not too long, this empty screen can be a perfectly acceptable user experience and no further optimizations or skeletons are necessary.
Scenario 2: Client Side Rendering (SPA)
A buffered page load can however be the starting point for a non-isomorphic client-side app, i.e. if the server sends a single script tag as the app entry point. The JavaScript app could then leverage skeletons as content placeholders.
Fully client-side rendered JavaScript applications (i.e. SPAs) such as this are currently out of scope for this documentation. We may add updates in the future. In general though, many of the concepts of out-of-order streaming will apply.
Scenario 3: In-Order Streaming (Progressive Rendering)
In contrast to buffered rendering, progressive rendering does not have to wait for the entire HTML to arrive from the server. The client receives the HTML in chunks, allowing critical above-the-fold content to be displayed first, meaning less time staring at an empty white screen. For the vast majority of pages using progressive rendering, skeleton placeholders will be unneccessary, as the nature and speed in which content is streamed is a sufficient UX improvement over the "white screen" encountered with buffered rendering.
The CSS :empty technique is highly efficient for this scenario and basic skeleton orchestration. Any kind of complex skeleton orchestration would be better left to out-of-order streaming. Remember also, that our Skin skeleton classes cannot be leveraged with this technique; therefore the developer must construct skeletons with their own custom CSS.
In all examples below we utilize client-side JavaScript to simulate how a server might stream the HTML in chunks to the client.
One Column with Empty Space
Our first progressive rendering example shows how the main article content might be streamed in after the main navigation and featured articles.
We have added a 3 second delay in order to simulate a service that is slow to fetch the main content. Remember, this is a highly contrived use case! No web site should be taking that long to retrieve static content and therefore would never require a skeleton!
One Column with Skeleton
Now let's add a skeleton placeholder for the main content. Using progressive rendering we stream an opening div tag to the client, and while the server works on getting the next chunk ready, the element will match the CSS :empty rule.
Okay, that was easy, our white space has been replaced with a skeleton while we wait for the next chunk to arrive. But what about when we have two columns?
Two Columns with Empty Space
To keep things simple lets assume the second column is also static content that will be quickly flushed from the server.
The goal of a skeleton is to replace empty space with a placeholder graphic giving a rough approximation of the content and layout to come. Lets see if we can update our skeleton to show two columns instead of just one.
Two Columns with Skeleton
Our main column and side column are siblings elements, so we cannot start the side column element without flushing and closing the main column element that precedes it. This means we cannot have an :empty main column and an :empty side column simultaneously.
With some clever CSS we can create a single background image graphic representing a two column layout on the :empty main column.
This might be satisfactory in some cases, but what if we want to show the main content without waiting for the side column to be flushed (the side content may be a slow service also)? What about responsive design? How will our two column skeleton respond at various breakpoints?
At this point we start to see that choreographing more than one skeleton placeholder with in-order streaming can become quite tricky. When we have more than one slow service, and a need for multiple skeleton placeholders, it is time to think about out-of-order streaming, which can eliminate these choreography concerns, but on the flipside has the potential to introduce the dreaded layout shift. Read on to find out more.
Scenario 4: Out-of-Order Streaming
Out-of-order streaming allows chunks of page content to arrive in any order, e.g. the server could send the chunk for the side column before the chunk for the main column. This technique does require some client-side JavaScript to re-assemble the DOM in the correct order as pieces arrive.
Out-of-order streaming also has parallels with client-side rendered SPAs, as those apps might typically render content based on the order in which their fetch requests come in.
In all examples below we utilize client-side JavaScript to simulate an out-of-order streaming experience.
Layout Shift
With our in-order streaming examples, the appereance of new content was always appended, therefore never caused existing content or layout to move. Let's see what happens when we stream in our main content, side column and footer out of order.
As the content is loading, try clicking on the link in the footer. Now imagine if it were an "Add to Cart" or "Buy Now" button and how infuriating that would be. Imagine being a keyboard user, and the element with keyboard focus being pushed down the page out of view
So, while getting content to the user faster always sounds like a good idea in theory, this example showcases how it can cause some unintended consequences and a poor UX.
The good news is that skeletons, under certain conditions, have the ability to mitigate or mask entirely the problems associated with content arriving out-of-order.
Reserving Space
Skeletons can prevent the layout shift associated with out-of-order streaming by reserving the physical space for that content.
The physical dimensions of the reserved space must be known ahead of time. When skeletons reserve the right amount of space, they will prevent layout from shifting. When they reserve too much or too little, layout will shift.
For some kinds of content, we can be sure of the physical dimensions ahead of time, images, videos, carousels, for example. For text however, we can only approximate in most cases, and therefore blocks of text make poor candidates for skeletons.
Skeleton-itis
With in order streaming and the :empty technique, it is not possible to display more than one skeleton concurrently. With out-of-order streaming, this restriction is lifted. This is both a good and a bad thing.
Let's see what happens when we create skeletons for all main page sections.
Compare this with our previous example or In-Order streaming. The wait time for the main content is the same, but without any shift or flashing in-and-out of skeletons. Most web pages should not have a 1:1 mapping of page sections to skeletons!
Out-of-order Placeholders Only
Perhaps there is a compromise? We have established that to avoid unexpected layout shift, skeletons must reserve exactly the same amount of space as the content. The exception to this rule is if the skeleton and its content is appended to the page.
Out-of-order streaming allows us to lay down our placeholders all at the same time and reserve space without waiting for the rest of the page. We can then sequence the streaming in a way that only ever appends the content.
While this might seem to defeat the purpose of out-of-order streaming (i.e. getting content onscreen quicker, regardless of its position) this does provide us with one method of tackling undesired layout shift and gaining some of the perceived performance increase of skeletons.
Scenario 5: Partial Page Updates
So far, our emphasis has been on the initial page load, but skeletons are also useful (perhaps even more so) for partial page updates, i.e. when a user interface component triggers a request for additional content from the server.
Two common examples are loading in more content to any section of the page (e.g. a "see more" button) and refreshing the content of a section (e.g. by changing a search filter).
It is important to note that any layout shift that happens immediately upon click of a button would not be classed as unexpected. However, if the layout shifts again, after some amount of time (i.e. after a wait for data to be fetched), then this would be classed as unexpected (because the user triggered that action). Again, skeletons can help in this scenario by reserving physical space until the actual content is ready.
Load More - Unpredictable Size
This example shows an initial expected layout shift upon clicking the load more button. After a 3 second wait we experience an unexpected layout shift (i.e. the button is shunted down the page) due to the skeletons not reserving the same amount of physical space as the content that is fetched.
Each item that is loaded has an arbitrary amount of text. How could we make it less arbitrary and more predictable?
Load More - Predictable Size
One way to make content length less arbitrary is via text truncation. In this example the content has been truncated to two lines, meaning we can be more sure about the physical space that needs to be reserved by the skeleton.
Again though it is worth remembering, working with text is far less predictable than with images, video and the like. The more predictable the type of content and its size and shape, the better skeletons will work to mitigate layout shift.
Refreshing Content
In addition to adding more content to any section of the page, another common scenario is to refresh the content of an existing area of the page. For example, paginating to the next set of results, or refreshing a set of thumbnail images.
Just as with loading more content, refreshing a section of content can fall afoul of layout shift if the content size is unpredictable. Again the situation can be avoided with predictable content size.