This article is part of a series on speeding up Shiny. Learn how to omit the server.r bottleneck and push actions to the browser in this article by Marcin Dubel. Learn how to diagnose poor Shiny performance, use faster functions, and take advantage of caching operations in this article by Krystian Igras.
Because of the way R Shiny is designed, long running calculations freeze the UI of Shiny dashboards until the calculations are complete. This can result in a sluggish app and a negative user experience. Appsilon has created a package to offload long running calculations to an external machine so that the UI of Shiny dashboards can remain responsive.
One of the performance challenges in Shiny is that long running calculations can paralyze your UI. You will not be able to change input values and parts of your application will be frozen until the heavy task is completed. This is an intentional feature of Shiny to avoid race conditions, but is nonetheless very frustrating for the end user.
Here is how Appsilon has solved the problem in our projects. We have developed a proprietary R package called shiny.worker, which is an abstraction based on futures, for delegating jobs to an external worker. While the job is running and the heavy calculation is being processed by the external worker, you can still interact with the app and the UI remains responsive.
Here is an example app where you can see shiny.worker in action: https://demo.appsilon.ai/apps/shiny-worker/
The idea of our solution is very simple. If I have long running calculations that are likely to freeze the UI of my app, then I delegate them to the worker. The worker is an external machine that executes R code.
Here is the code of the shiny.worker demo app:
The key fragment is the line with the shiny.worker::job() call where you schedule a job. The job is your long running calculation. When it was a regular reactive, it was blocking the UI, but now it is delegated to the worker. As the calculation is delegated to the worker, the UI is unfrozen while the calculation is being performed.
Arguments for the job are provided as a reactive (trigger_args). Its value will be passed to the job function as args. This means that every time the value of this reactive changes, shiny.worker will take action, depending on the strategy you choose. It can be triggering a new job and cancelling a running job or ignoring the change (no new job is scheduled until it is resolved). It is the developer’s responsibility to implement app logic to avoid potential race conditions.
To access the worker’s result, you call it like you do with a reactive (plotValuesPromise()). As a result you are able to read its state (task$resolved) and returned value (task$result). You decide what should be returned when the job is still running with the argument value_until_not_resolved.
shiny.worker has not yet been released to the public, but we have made it available to several of our clients. So far, we’ve seen dramatic improvements in UX after implementing shiny.worker. If you are experiencing problems with Shiny performance due to heavy calculations, reach out to us and we will be happy to help you make use of shiny.worker.