A Long List need not actually be Long
Let's look at what makes and characterizes a list on a webpage:
- One or more elements with meaningful content inside
- Organized or ordered by one or more rules (time, alphabet, popularity, importance)
- There are only so many elements we can display in a viewport to say they're relevant, legible, or otherwise meaningful in some way to the reader.
- Scrolling up and down a list feels natural; a element's position on screen, relative to others, is generally fixed and unchanging—which is to say that we care about an element's persistence throughout the document.
- The browser window, or container of our elements usually takes care of hiding/showing elements—we need only supply elements.
There is a cost to playing the magician if we are to not rely on the viewport to show/hide elements when we need them for any given scroll position. The more we explore alternative ways of maintaining a long-document illusion, the more we must think about nuanced positioning for which we are now responsible.
Perhaps that effort exceeds the savings in drawing hundreds of DOM nodes in a message stream. However, this exercise seeks to stretch the imagination a bit, and—perhaps it can lead to other interesting solutions for experiences with the ever-growing lists we now consume.
DOM Elements Dance
We need a way to scroll down our page, and maintain an actual scroll position as if we had kept all of our messages we've scrolled past. If we don't somehow replace the space lost, we are simply swapping nodes and we never give ourselves a document that grows with each scroll.
As luck would have it, we do know the height of our swapped messages. Since we're iterating through those swapped blocks of messages, we can also keep track of the overall height lost, and figure out a simple way to keep our document afloat as we scroll further down.
Sentinels!
Surprisingly, that should cover basic scrolling through long lists of messages. With relatively little effort we can shuffle a handful of elements, quite quickly, and with no sight of edges of the smoke and mirrors involved.
Optimizations and Further Implications
The aforementioned cost presents itself to be paid in this section. We're not just scrolling through a long document of infinitely-added elements. We divide a fixed set of elements, introduce states of transition, and rely on correct arithmetic to choreograph our messages to dance in unison. JavaScript, waving its best flag, gives us flexibility and smoothness in its async execution, but that manifests itself in quite the unexpected behaviour.
Dynamic Heights
Impatient Events Queued into Submission
One of the now popular gestures associated with list in web applications is a swipe. This is not new to our daily routines with paper documents, dust on our tables, or bugs in our face. If we do not like something, we swipe it away, out of sight and out of mind. So, when we introduce swiping to our list, we almost certainly think of dismissing a particular message.
The thought is relatively short-lived in our minds, and our expectation is simply for our wish to be fulfilled. To our program, however, our wish can only be fulfilled by the exact conditions we specify.
Simple conditionals dictate what we should do with a message when it is swiped. A swipe event meets a minimum threshold, the message moves off-screen, done. Well, not quite. We want to keep the actual DOM element we've removed from view, and replace it with something more meaningful for later viewing. That's fine. We can move the message to end of the list and queue up our next message, from the network, or cache. Still nothing really novel.
But, suppose we are impatient and swipe several messages from view and quickly scroll up or down in the document.
In the time, or state, where our messages are being removed from the list, we scroll down, and we tell our program to swap a block of messages from the top, to the bottom. The program must interpret the top most set as 'ready for swap,' yet we could have a swiped message in that set. So, before we've removed and replaced the swiped message, we swap it to the bottom-most position from a competing scroll operation.
This breaks continuity in most cases, as whichever operation completes first, the following one operates on an outdated state and populates the replaced node with incorrect data.
The precedence of swipe over scroll can likely be proved trivial, but in practice, the scroll queue never appears to reach more than one operation—likely because we can't scroll that fast, but more importantly, because we shouldn't be able to scroll past an unfulfilled scroll-swap operation to even need another. That would imply that we've reached past the visible nodes of our growing/shrinking document, and would undo what we're trying to build.
A technical implementation of this queue can be reduced to nothing more than a RequestAnimationFrame
loop operating within the scope a few states:
Each pending request is then constructed of the following:
Subsequently, a function operates on the accumulated requests, in-order, until they are all fulfilled, and the requests array is emptied. So long as our run
function takes less than the 16ms
it should take for a RequestAnimationFrame
to complete before the next RAF is called, we should be able to maintain strong synchronization among our draws, swaps, scrolls, and swipes.
So, with a queue instantiated for each distinct action, scroll and swipe, if we have a running swipe queue, we hold off on replacing DOM nodes for a scroll event that may be requested. We fulfill the scroll request after the swipe operation has settled our recently swapped/replaced DOM element, and allow it to operate on the most current state. This appears to happen fast enough for a practical amount of swiped messages. It can likely be broken, but likely not at the speed with which normal fingers can operate.
Network Optimizations:
Since our presentation of messages, under-the-hood, is more akin to pagination, we can leverage our dynamic calls for new messages in at least these fashions:
- We're already looping through our messages, so we can push to an array with message contents and any other meaningful attributes about it—replacing said info from the cached array on subsequent swaps. So, in the same operation, we simply check to see if the message is already available, and populate data from the cache, rather than sending off another XHR.
- We can extend the reach and power of our cache to utilise robust search by relying on cached data, rather than server-side look-ups. So, where a robust lookup may take longer to fulfill, a similar algorithm can be executed clientside with the already-available data. Whether this is actually true becomes evident in future iterations of a tool like our dynamic list.
The above satisfy server load concerns, but what about user experiences in slow-internet conditions. The state of mobile Internet in the very country that invented it can be—slow. At least for our scope, a set of messages is usually a very small amount of data. However, user avatars, gifs, images, and other engaging content can amount to more expense.
Tombstones over Spinners!
Furthermore, the whizzing motion of the tombstone demonstrates motion in general—exactly what a spinner is meant to accomplish. Where the latter is simply a visual cue that some request is being fulfilled, the former is interactive and can delegate multiple requests, with more granularity to changes in direction, speed, and precision.
This construct not only maintains continuity in the visual sensation we get from scrolling, but also allows for a more robust search when those messages finally resolve to data. Where the former allows the user to actually associate their actions with instantaneous feedback of a scrolling list of elements, the latter opens up possibilities in search that are often ill-represented in search/filter applications.
When we search our list, we usually recall a few words, and hope to find those words in a message we or someone else mentioned in the last couple days. So instead of waiting for a single set to be retrieved, we retrieve several sets with a continuous gesture, and then go jazz-hands on the search bar. In fact, this style of fetch a bunch—search when done dynamic can allow us to quickly realize what it is we want from the list. The eventual search operation can then programmatically scroll us right over to the message(s) we are hoping to find.
These are simply ideas for the direction a project like this would go; After some thought, implementing tombstones by resolving a Promise
bound to some DOM element is likely just the simple case. Were we to scroll past a few swap operations, the node to which we would resolve our data would already have been swapped and may be in a different place entirely. We would need more logic to dismiss the promised data and populate from the relevant request. That is not to say this is impossible!