Improving the responsiveness of Shiny applications
What do we mean by “responsiveness”?
Confusingly (and rather unhelpfully) when it comes to web applications there are two different topics that may be referred to by the terms “responsive” or “responsiveness”. If you stick “responsive UI” into your favourite search engine the top results will concern “responsive design” - the practice of making websites and applications work across devices, regardless of device and browser dimensions. That’s an interesting and important topic when it comes to designing data-science applications but it’s not what we’re covering here.
What we’re covering here is responsiveness that you might stick “un” in front of if things got really bad. It’s about making your user interface feel like it responds instantaneously to a user’s interaction. We’ll go from covering clicking a button and making sure the user sees some kind of simple acknowledgement the button has been clicked to clicking a button (or dragging a slider or…) and immediately seeing the results of complex computations.
While related, responsiveness isn’t the same as performance. Performance is about completing an operation in the minimal amount of time, while responsiveness is about meeting human needs for feedback when executing an action.
Motivation
If you’ve made a quick and simple {shiny} app as a toy for your own benefit maybe you shouldn’t care that much if it “feels” responsive. But if you’re making an application you want to be used by others you presumably want them to take something from it. That could be “Wow, I finally understand Fourier Transforms” (I would love someone to make an app that could make me say that), or “Clearly I need to buy this product/service from this company” or simply “This person is very clever, we should hire them”. You’re not likely to get those kinds of responses if your app is unresponsive to interaction. If your user clicks something and nothing happens as far as they can see - they don’t know that somewhere, perhaps the other side of the world, there’s a server performing millions of operations for their benefit - they’ll feel frustrated and likely give up pretty quickly.
“Guru of website usability” Jakob Nielsen wrote that responsiveness matters because of
Human limitations, especially in the areas of memory and attention… We simply don’t perform as well if we have to wait and suffer the inevitable decay of information stored in short-term memory.
Human aspirations. We like to feel in control of our destiny rather than subjugated to a computer’s whims. Also, when companies make us wait instead of providing responsive service, they seem either arrogant or incompetent.
Jeff Johnson gives more details in his excellent Designing with the Mind in Mind. From page 246 of the third edition:
If software waits longer than 0.1 second to show a response to a user’s action, the perception of cause and effect is broken; the software’s reaction will not seem to be a result of the user’s action. Meeting the 0.1 second deadline is essential to support users’ perceptions that they are directly manipulating objects on the display. Therefore, onscreen buttons have 0.1 second to show they’ve been clicked; otherwise, users will assume they missed and click again. This does not mean that buttons have to complete their function in 0.1 second — only that they must show that they have been pressed by that deadline.
The simplest improvements you can make
Simply making sure the user is aware their click or tap was registered and something is happening is a great first step. Probably the simplest of all options is to set the cursor style to “wait” when the app is busy. Assuming your application already has a custom-CSS file, that’s as simple as adding something like the following:
html.shiny-busy .container-fluid {
cursor: wait;
}
The biggest issue here is it only works if the user has a visible cursor. For something that works on touch devices and is easy to add, you might want to take a look at {shinycssloaders} (see below) and/or {waiter} that we covered in our article on Top 5 Shiny UI Add-On Packages.
If the user might have to wait longer than a few seconds for the process they’ve just set in motion to complete, you should consider a progress indicator. From page 247 of DwtMiM:
Because 1 second is the maximum gap expected in conversation, and because operating an interactive system is a form of conversation, interactive systems should avoid lengthy gaps in their side of the conversation. Otherwise, the human user will wonder what is happening. Systems have about 1 second to either do what the user asked or indicate how long it will take. Otherwise, users get impatient.
If an operation will take more than a few seconds, a progress indicator is needed. Progress indicators are an interactive system’s way of keeping its side of the expected conversational protocol: “I’m working on the problem. Here is how much progress I have made and an indication of how much more time it will take.”
The {shiny} package itself has a progress indicator, while the {waiter} package also offers a nice built-in-to-button option, shown below:
Clearing performance bottlenecks
As already mentioned, responsiveness and performance are two different aspects of the user experience. Nevertheless, improving the performance of an app can lessen the need to provide extra feedback like loading indicators.
If you do find that the user has to wait for an extended period it may be worth rethinking the amount of processing that your shiny app is performing: could you reduce the amount of computation occuring inside the app (by caching plots and tables, or by precomputing your data), could the app be using too much reactivity or regenerating UI elements unnecessarily? We’ve previously discussed how we took the loading times of a World Health Organization app from minutes(!) to seconds. The Shiny documentation also gives advice on how the likes of caching and async programming can be used to bolster performance. Chapter 15 of Engineering Production-Grade Shiny Apps covers, in detail, some common performance pitfalls and how to solve them.
Taking the next step with {htmlwidgets}
If you want your app to feel like it responds instantaneously to interactions then you probably need to look at {htmlwidgets}. By moving some of the work from a Shiny server to the user’s own browser these can, in principle at least, provide instant (~0.1 seconds or less) updates - typically to a visualisation or table - in response to a user’s interactions. The result is an enhanced feeling of direct manipulation:
Direct manipulation (DM) is an interaction style in which users act on displayed objects of interest using physical, incremental, reversible actions whose effects are immediately visible on the screen.
There are an ever-increasing number of such widgets available off-the-shelf. From {plotly} charts, to {leaflet} maps, to {DT} data tables. Use of the latter is shown in the video below, where filtering and sorting occurs instantly following my interactions. There’s no lag where I might get distracted and forget what I just did and if I err I don’t have to wait for a server to tell me so before I retrace my steps.
Most HTML widgets are wrappers around pre-existing JavaScript libraries. That means you could get the same widget functionality without using R and Shiny at all. But the power of htmlwidgets is how easily they can fit into your extant workflows, typically requiring just a few lines of R code. This is advantageous because you use the widgets without having to understand the underlying web technologies, but also because R has a much richer ecosystem for data science than JavaScript. By using them together you can harness the benefits of both. If you want to create your own widgets, however, you do need some knowledge of HTML, CSS and JavaScript. That’s beyond the scope of this article, but if you are interested to learn more then John Coene’s JavaScript for R book is thorough and free to read online.
Widgets and animation
Moving work from server to browser makes it more practical to use another pedagogical tool: animation. After all, things would likely feel pretty clunky to the user if they clicked a button in the UI, then had to wait while data was sent to and back-from a server before animations finally kicked off.
When it comes to animated visualisations, you’re probably already familiar with Hans Rosling’s talks using the Trendalyzer tool from Gapminder, for example The best stats you’ve ever seen. From these we see that with animations it’s easy to track a point or collection of points in a two-dimensional scatter plot as a third dimension (such as time) changes.
But Heer and Robertson found that, with a bit of care, animations can be used to aid much more drastic changes: going from one chart form to another, e.g. a scatter plot to a bar chart or a bar chart to a donut. They also used animations to show data drilldown - in this case going from an individual bars to stacked bar and on to grouped bars - and found both a reduction in user error performing some simple tasks and a strong user preference for the use of these animations.
In a recent project with Utah Tech University we built a pair of custom widgets - using the popular d3 JavaScript library - for embedding in Shiny dashboards, visualising student admission and retention data. The video below shows the JavaScript library we built for the widgets in action - a Sankey (or, more precisely, alluvial) diagram - but repurposed with the diamonds data from the {ggplot2} package that you may be more familiar with. The library offers immediate feedback with a customisable popup and link and node highlighting when hovering over any link or node. Moreover, clicking on one of the nodes drills down on that node - revealing information about the clarity for the selected cut in the case shown - and updates other preceding and succeeding steps of the diagram accordingly. Shift-clicking reverses the process.
All the requisite data processing is done “on-the-fly”, in the browser and effectively instantaneously (in a few milliseconds) so that, to invert Johnson’s language above, the user has the perception of cause and effect; the software’s reaction seems to be a result of the user’s action.
You can find out much more about our work with Utah Tech by watching Theo’s talk “Expect the Unexpected - {Shiny} & {htmlwidgets}” from Shiny in Production 2022. The JavaScript code used in the widgets is open source and available on GitHub.
When {htmlwidgets} and Shiny applications might not be the answer
User experience design is often a case of picking the right tools for the job and there are, of course, times when htmlwidgets may not be the right tools.
Simple loading and/or progress indicators might be all that’s realisable given project constraints on time and/or money. The round trips to a Shiny server may be necessary because the relevant calculations you need to run to update data rely on some complex R (or Python!) code that would be difficult to run in JavaScript. Alternatively, it might be the underlying data itself that is the issue - it needs to be kept private or it might be impractically large to transfer it to a user’s browser to process.
htmlwidgets are quick and easy to add to a project if they already exist, but creating new ones requires additional developer expertise that you may not have in a team focussed on R or Python development.
It could be that the best solution to a UX problem is simple, static content. As Jakob Nielsen reported back in 2010:
Instead of big images, today’s big response-time sinners are typically overly complex data processing on the server or overly fancy widgets on the page (or too many fancy widgets).
Static content has other advantages - it’s largely agnostic to device size and form, it’s probably easier to make accessible and it’s almost certainly quicker and easier to assemble and maintain. As Colin Fay pointed out in his Shiny in Production 2022 talk, interactive elements also have a learning curve associated with them that you don’t find with a png image or a simple HTML table. Direct manipulation only works if the user understands what and how to manipulate.
Summary
Improving the responsiveness of a Shiny application can help the user feel more like they are in control and that they are using professional software created by thoughtful designers. Greater responsiveness can be achieved through simple changes like loading indicators and progress bars or through the use of htmlwidgets which move some of the burden from the Shiny server to the user’s own browser. However, interactive applications do have a maintenance cost to the designer and a learning curve for the user. It is worthwhile considering whether these costs are justified before starting a project.