Mastering Shiny








Learning Progress: Completed.🎉

Getting started

1 Your first Shiny app

  • You can enter that URL into any compatible web browser to open another copy of your app.

  • Layout functions, inputs, and outputs have different uses, but they are fundamentally the same under the covers: they’re all just fancy ways to generate HTML, and if you call any of them outside of a Shiny app, you’ll see HTML printed out at the console. Don’t be afraid to poke around to see how these various layouts and controls work under the hood.

  • The essence of reactivity: outputs automatically react (recalculate) when their inputs change.

  • In every kind of programming, it’s poor practice to have duplicated code; it can be computationally wasteful, and more importantly, it increases the difficulty of maintaining or debugging the code.

  • You create a reactive expression by wrapping a block of code in reactive({...}) and assigning it to a variable, and you use a reactive expression by calling it like a function. But while it looks like you’re calling a function, a reactive expression has an important difference: it only runs the first time it is called and then it caches its result until it needs to be updated.

2 Basic UI

  • Generally, I recommend only using sliders for small ranges, or cases where the precise value is not so important. Attempting to precisely select a number on a small slider is an exercise in frustration!

  • By default, plotOutput() will take up the full width of its container (more on that shortly), and will be 400 pixels high. You can override these defaults with the height and width arguments. We recommend always setting res = 96 as that will make your Shiny plots match what you see in RStudio as closely as possible.

3 Basic reactivity

  • The key idea of reactive programming is to specify a graph of dependencies so that when an input changes, all related outputs are automatically updated.

  • The ui is simple because every user gets the same HTML. The server is more complicated because every user needs to get an independent version of the app; when user A moves a slider, user B shouldn’t see their outputs change.

    To achieve this independence, Shiny invokes your server() function each time a new session starts. Just like any other R function, when the server function is called it creates a new local environment that is independent of every other invocation of the function. This allows each session to have a unique state, as well as isolating the variables created inside the function. This is why almost all of the reactive programming you’ll do in Shiny will be inside the server function.

  • Unlike a typical list, input objects are read-only. If you attempt to modify an input inside the server function, you’ll get an error.

  • One more important thing about input: it’s selective about who is allowed to read it. To read from an input, you must be in a reactive context created by a function like renderText() or reactive(). It’s an important constraint that allows outputs to automatically update when an input changes.

  • It’s important to understand that the order your code run is solely determined by the reactive graph. This is different from most R code where the execution order is determined by the order of lines.

  • In Shiny, however, I think you should consider the rule of one: whenever you copy and paste something once, you should consider extracting the repeated code out into a reactive expression. The rule is stricter for Shiny because reactive expressions don’t just make it easier for humans to understand the code, they also improve Shiny’s ability to efficiently rerun code.

Shiny in action

4 Workflow

  • Improving workflow is a good place to invest time because it tends to pay great dividends in the long run. It doesn’t just increase the proportion of your time spent writing R code, but because you see the results more quickly, it makes the process of writing Shiny apps more enjoyable, and helps your skills improve more quickly.

  • The basic idea of print debugging is to call print() whenever you need to understand when a part of your code is evaluated, and to show the values of important variables. We call this “print” debugging (because in most languages you’d use a print function), but In R it makes more sense to use message() :

    • print() is designed for displaying vectors of data so it puts quotes around strings and starts the first line with [1].

    • message() sends its result to “standard error”, rather than “standard output”. These are technical terms describing output streams, which you don’t normally notice because they’re both displayed in the same way when running interactively. But if your app is hosted elsewhere, then output sent to “standard error” will be recorded in the logs.

5 Layout, themes, HTML

6 Graphics

  • For hosted apps, you also have to take into account the time needed to transmit the event from the browser to R, and then the rendered plot back from R to the browser.

7 User feedback

  • An even simpler alternative is to use the shinycssloaders package by Dean Attali. It uses JavaScript to listen to Shiny events, so it doesn’t even need any code on the server side. Instead, you just use shinycssloaders::withSpinner() to wrap outputs that you want to automatically get a spinner when they have been invalidated.

  • There are a few small, but important, details to consider when creating a dialog box:

    • What should you call the buttons? It’s best to be descriptive, so avoid yes/no or continue/cancel in favour of recapitulating the key verb.

    • How should you order the buttons? Do you put cancel first (like the Mac), or continue first (like Windows)? Your best option is to mirror the platform that you think most people will be using.

    • Can you make the dangerous option more obvious? Here I’ve used class = "btn btn-danger" to style the button prominently.

8 Uploads and downloads

  • By default, the user can only upload files up to 5 MB. You can increase this limit by setting the shiny.maxRequestSize option prior to starting Shiny. For example, to allow up to 10 MB run options(shiny.maxRequestSize = 10 * 1024^2).

  • I recommend using .tsv (tab separated value) instead of .csv (comma separated values) because many European countries use commas to separate the whole and fractional parts of a number (e.g. 1,23 vs 1.23). This means `they can’t use commas to separate fields and instead use semi-colons in so-called “c”sv files! You can avoid this complexity by using tab separated files, which work the same way everywhere.

  • By default, RMarkdown will render the report in the current process, which means that it will inherit many settings from the Shiny app (like loaded packages, options, etc). For greater robustness, I recommend running render() in a separate R session using the callr package:

    render_report <- function(input, output, params) {
        output_file = output,
        params = params,
        envir = new.env(parent = globalenv())
    server <- function(input, output) {
      output$report <- downloadHandler(
        filename = "report.html",
        content = function(file) {
          params <- list(n = input$slider)
            list(input = report_path, output = file, params = params)

9 Dynamic UI

  • There are three key techniques for creating dynamic user interfaces:

    • Using the update family of functions to modify parameters of input controls.

    • Using tabsetPanel() to conditionally show and hide parts of the user interface.

    • Using uiOutput() and renderUI() to generate selected parts of the user interface with code.

  • You might wonder when you should use freezeReactiveValue(): it’s actually good practice to always use it when you dynamically change an input value. The actual modification takes some time to flow to the browser then back to Shiny, and in the interim any reads of the value are at best wasted, and at worst lead to errors. Use freezeReactiveValue() to tell all downstream calculations that an input value is stale and they should save their effort until it’s useful.

  • If you run this code yourself, you’ll notice that it takes a fraction of a second to appear after the app loads. That’s because it’s reactive: the app must load, trigger a reactive event, which calls the server function, yielding HTML to insert into the page. This is one of the downsides of renderUI(); relying on it too much can create a laggy UI. For good performance, strive to keep fixed as much of the user interface as possible, using the techniques described earlier in the chapter.

    There’s one other problem with this approach: when you change controls, you lose the currently selected value. Maintaining existing state is one of the big challenges of creating UI with code. This is one reason that selectively showing and hiding UI is a better approach if it works for you — because you’re not destroying and recreating the controls, you don’t need to do anything to preserve the values. However, in many cases, we can fix the problem by setting the value of the new input to the current value of the existing control:

    server <- function(input, output, session) {
      output$numeric <- renderUI({
        value <- isolate(input$dynamic)
        if (input$type == "slider") {
          sliderInput("dynamic", input$label, value = value, min = 0, max = 10)
        } else {
          numericInput("dynamic", input$label, value = value, min = 0, max = 10)

10 Bookmarking

  • By default, Shiny apps have one major drawback compared to most web sites: you can’t bookmark the app to return to the same place in the future or share your work with someone else with a link in an email. That’s because, by default, Shiny does not expose the current state of the app in its URL.

  • There are three things we need to do to make this app bookmarkable:

    1. Add a bookmarkButton() to the UI. This generates a button that the user clicks to generate the bookmarkable URL.

    2. Turn ui into a function. You need to do this because bookmarked apps have to replay the bookmarked values: effectively, Shiny modifies the default value for each input control. This means there’s no longer a single static UI but multiple possible UIs that depend on parameters in the URL; i.e. it has to be a function.

    3. Add enableBookmarking = "url" to the shinyApp() call.

  • Instead of providing an explicit button, another option is to automatically update the URL in the browser. This allows your users to use the user bookmark command in their browser, or copy and paste the URL from the location bar.

    Automatically updating the URL requires a little boilerplate in the server function:

    # Automatically bookmark every time an input changes
    # Update the query string

11 Tidy evaluation

  • It’s going to be easier to understand what’s happening if we can disambiguate the two uses by introducing two new terms:

    • An env-variable (environment variable) is a “programming” variables that you create with <-. input$var is a env-variable.

    • A data-variable (data frame variables) is “statistical” variable that lives inside a data frame. carat is a data-variable.

    With these new terms we can make the problem of indirection more clear: we have a data-variable (carat) stored inside an env-variable (input$var), and we need some way to tell dplyr this. There are two slightly different ways to do this depending on whether the function you’re working with is a “data-masking” function or a “tidy-selection” function.

  • Data-masking functions allow you to use variables in the “current” data frame without any extra syntax. It’s used in many dplyr functions like arrange(), filter(), group_by(), mutate(), and summarise(), and in ggplot2’s aes(). Data-masking is useful because it lets you use data-variables without any additional syntax.

  • Fortunately, inside data-masking functions you can use .data or .env if you want to be explicit about whether you’re talking about a data-variable or an env-variable:

    diamonds %>% filter(.data[[var]] > .env$min)
  • However, in my opinion, one of the advantages of the tidyverse is the careful thought that has been applied to edge cases so that functions work more consistently.

  • As well as data-masking, there’s one other important part of tidy evaluation: tidy-selection. Tidy-selection provides a concise way of selecting columns by position, name, or type. It’s used in dplyr::select() and dplyr::across(), and in many functions from tidyr, like pivot_longer(), pivot_wider(), separate(), extract(), and unite().

  • To refer to variables indirectly use any_of() or all_of(): both expect a character vector env-variable containing the names of data-variables. The only difference is what happens if you supply a variable name that doesn’t exist in the input: all_of() will throw an error, while any_of() will silently ignore it.

Mastering reactivity

12 Why reactivity?

  • But magic in software usually leads to disillusionment: without a solid mental model, it’s extremely difficult to predict how the software will act when you venture beyond the borders of its demos and examples. And when things don’t go the way you expect, debugging is almost impossible.

  • Once you’ve formed an accurate mental model of reactivity, you’ll see that there’s nothing up Shiny’s sleeves: the magic comes from simple concepts combined in consistent ways.

  • Reactive programming is a style of programming that focuses on values that change over time, and calculations and actions that depend on those values.

  • For Shiny apps to be maximally useful, we need reactive expressions and outputs to update if and only if their inputs change. We want outputs to stay in sync with inputs, while ensuring that we never do more work than necessary.

  • A reactive expression has two important properties:

    • It’s lazy: it doesn’t do any work until it’s called.

    • It’s cached: it doesn’t do any work the second and subsequent times it’s called because it caches the previous result.

  • Spreadsheets are closely related to reactive programming: you declare the relationship between cells using formulas, and when one cell changes, all of its dependencies automatically update. So you’ve probably already done a bunch of reactive programming without knowing it!

13 The reactive graph

  • Recall that reactive inputs and expressions are collectively called reactive producers; reactive expressions and outputs are reactive consumers.

  • You might wonder how Shiny decides which of the invalidated outputs to execute. In short, you should act as if it’s random: your observers and outputs shouldn’t care what order they execute in, because they’ve been designed to function independently.

  • Shiny records a relationship between the output and reactive expression (i.e. we draw an arrow). The direction of the arrow is important: the expression records that it is used by the output; the output doesn’t record that it uses the expression. This is a subtle distinction, but its importance will become more clear when you learn about invalidation.

  • Each invalidated reactive expression and output “erases” all of the arrows coming in to and out of it and completing the invalidation phase.

  • It may seem perverse that we put so much value on those relationships, and now we’ve thrown them away! But this is a key part of Shiny’s reactive programming model: though these particular arrows were important, they are now out of date. The only way to ensure that our graph stays accurate is to erase arrows when they become stale, and let Shiny rediscover the relationships around these nodes as they re-execute.

  • The reactive dependency is established when you read a value from input, not when you use that value.

14 Reactive building blocks

  • It’s important to note that both types of reactive values have so called reference semantics. Most R objects have copy-on-modify48 semantics which means that if you assign the same value to two names, the connection is broken as soon as you modify one. This is not the case with reactive values — they always keep a reference back to the same value so that modifying any copy modifies all values.

  • Reactive expressions cache errors in exactly the same way that they cache values.

  • Errors are also treated the same way as values when it comes to the reactive graph: errors propagate through the reactive graph exactly the same way as regular values. The only difference is what happens when an error hits an output or observer:

    • An error in an output will be displayed in the app.

    • An error in an observer will cause the current session to terminate. If you don’t want this to happen, you’ll need to wrap the code in try() or tryCatch().

  • Observers and outputs are powered by the same underlying tool: observe(). This sets up a block of code that is run every time one of the reactive values or expressions it uses is updated. Note that the observer runs immediately when you create it — it must do this in order to determine its reactive dependencies.

  • observe() also powers reactive outputs. Reactive outputs are a special type of observers that have two important properties:

    • They are defined when you assign them into output, i.e. output$text <- ... creates the observer.

    • They have some limited ability to detect when they’re not visible (i.e. they’re in non-active tab) so they don’t have to recompute51.

    It’s important to note that observe() and the reactive outputs don’t “do” something, but “create” something (which then takes action as needed).

  • Shiny provides isolate() to resolve this problem. This function allows you to access the current value of a reactive value or expression without taking a dependency on it

  • invalidateLater(ms) causes any reactive consumer to be invalidated in the future, after ms milliseconds. It is useful for creating animations and connecting to data sources outside of Shiny’s reactive framework that may be changing over time.

15 Escaping the graph

  • Having a reactive graph that is as simple as possible is important for both humans and for Shiny. A simple graph is easier for humans to understand, and a simple graph is easier for Shiny to optimise.

Best practices

16 General guidelines

  • Improving your software engineering skills is a lifelong journey. Expect to have frustrations as you start learning them, but understand that everyone experiences the same issues, and if you persevere you’ll get past them. Most people go through the same evolution when learning a new technique: “I don’t understand it and have to look it up every time I use it” to “I vaguely understand it but still read the documentation a lot” to eventually “I understand it and can use it fluidly”. It takes time and practice to get to the final stage.

  • Being a good programmer means developing empathy for others who will need to interact with this code-base in the future (even if it’s just future-you!). Like all forms of empathy, this takes practice and becomes easier only after you’ve done it many times. Over time, you’ll start to notice that certain practices improve the readability of your code. There are no universal rules, but some general guidelines include:

    • Are the variable and function names clear and concise? If not, what names would better communicate the intent of the code?

    • Do I have comments where needed to explain complex bits of code?

    • Does this whole function fit on my screen or could it be printed on a single piece of paper? If not, is there a way to break it up into smaller pieces?

    • Am I copying-and-pasting the same block of code many times throughout my app? If so, is there a way to use a function or a variable to avoid the repetition?

    • Are all the parts of my application tangled together, or can I manage the different components of my application in isolation?

  • Automation takes time to set up, but it pays off over time because you can run the tests more frequently. For that reason, various forms of automated testing have been developed for Shiny, as outlined in Chapter 21. As that chapter will explain, you can develop:

    • Unit tests that confirm the correct behaviour of an individual function.

    • Integration tests to confirm the interactions between reactives.

    • Functional tests to validate the end-to-end experience from a browser

    • Load tests to ensure that the application can withstand the amount of traffic you anticipate for it.

    The beauty of writing an automated test is that once you’ve taken the time to write it, you’ll never need to manually test that portion of the application again. You can even leverage continuous integration to run these tests every time you make a change to your code before publishing the application.

17 Functions

  • In this chapter, you’ll learn how writing functions can help. This tends to have slightly different flavours for UI and server components:

    • In the UI, you have components that are repeated in multiple places with minor variations. Pulling out repeated code into a function reduces duplication (making it easier to update many controls from one place), and can be combined with functional programming techniques to generate many controls at once.

    • In the server, complex reactives are hard to debug because you need to be in the midst of the app. Pulling out a reactive into a separate function, even if that function is only called in one place, makes it substantially easier to debug, because you can experiment with computation independent of reactivity.

    Functions have another important role in Shiny apps: they allow you to spread out your app code across multiple files. While you certainly can have one giant app.R file, it’s much easier to manage when spread across multiple files.

  • There are two places you might put them depending on how big they are:

    • I recommend putting large functions (and any smaller helper functions that they need) into their own R/{function-name}.R file.

    • You might want to collect smaller, simpler, functions into one place. I often use R/utils.R for this, but if they’re primarily used in your ui you might use R/ui.R.

  • If you’re developing a lot of Shiny apps within your organisation, you can help improve cross-app consistency by putting functions like this in a shared package.

  • Whenever you have a long reactive (say >10 lines) you should consider pulling it out into a separate function that does not use any reactivity. This has two advantages:

    • It is much easier to debug and test your code if you can partition it so that reactivity lives inside of server(), and complex computation lives in your functions.

    • When looking at a reactive expression or output, there’s no way to easily tell exactly what values it depends on, except by carefully reading the code block. A function definition, however, tells you exactly what the inputs are.

    The key benefits of a function in the UI tend to be around reducing duplication. The key benefits of functions in a server tend to be around isolation and testing.

18 Shiny modules

  • Shiny modules have two big advantages. Firstly, namespacing makes it easier to understand how your app works because you can write, analyse, and test individual components in isolation. Secondly, because modules are functions they help you reuse code; anything you can do with a function, you can do with a module.

histogramUI <- function(id) {
    selectInput(NS(id, "var"), "Variable", choices = names(mtcars)),
    numericInput(NS(id, "bins"), "bins", value = 10, min = 1),
    plotOutput(NS(id, "hist"))

histogramServer <- function(id) {
  moduleServer(id, function(input, output, session) {
    data <- reactive(mtcars[[input$var]])
    output$hist <- renderPlot({
      hist(data(), breaks = input$bins, main = input$var)
    }, res = 96)

histogramApp <- function() {
  ui <- fluidPage(
  server <- function(input, output, session) {
  shinyApp(ui, server)

  • Namespacing turns modules into black boxes. From outside of the module, you can’t see any of the inputs, outputs, or reactives inside of it.

  • Note that the module UI and server differ in how the namespacing is expressed:

    • In the module UI, the namespacing is explicit: you have to call NS(id, "name") every time you create an input or output.

    • In the module server, the namespacing is implicit. You only need to use id in the call to moduleServer() and then Shiny automatically namespaces input and output so that in your module code input$name means the input with name NS(id, "name").

  • Unlike regular Shiny code, connecting modules together requires you to be explicit about inputs and outputs. Initially, this is going to feel tiresome. And it’s certainly more work than Shiny’s usual free-form association. But modules enforce specific lines of communication for a reason: they’re a little more work to create, but much easier to understand, and allow you to build substantially more complex apps.

  • You might see advice to use session$userData or other techniques to break out of the module straitjacket. Be wary of such advice: it’s showing you how to work around the rules imposed by namespacing, making it easy to re-introduce much complexity to your app and significantly reducing the benefits of using a module in the first place.


datasetInput <- function(id, filter = NULL) {
  names <- ls("package:datasets")
  if (!is.null(filter)) {
    data <- lapply(names, get, "package:datasets")
    names <- names[vapply(data, filter, logical(1))]

  selectInput(NS(id, "dataset"), "Pick a dataset", choices = names)

datasetServer <- function(id) {
  moduleServer(id, function(input, output, session) {
    reactive(get(input$dataset, "package:datasets"))

datasetApp <- function(filter = NULL) {
  ui <- fluidPage(
    datasetInput("dataset", filter = filter),
  server <- function(input, output, session) {
    data <- datasetServer("dataset")
    output$data <- renderTable(head(data()))
  shinyApp(ui, server)

  • You can make the life of module user much easier with a quick and dirty call to stopifnot(). For example, selectVarServer() could check that data is reactive and filter is not with the following code:

    selectVarServer <- function(id, data, filter = is.numeric) {
      moduleServer(id, function(input, output, session) {
        observeEvent(data(), {
          updateSelectInput(session, "var", choices = find_vars(data(), filter))
  • The key challenge of creating modules is creating functions that are flexible enough to be used in multiple places, but simple enough that they can easily be understood. Figuring out how to write functions that are good building blocks is the journey of a lifetime; expect that you’ll have to do it wrong quite a few times before you get it right. (I wish I could offer more concrete advice here, but currently this is a skill that you’ll have to refine through practice and conscious reflection.)

  • The main challenge with this sort of code is remembering when you use the reactive (e.g. x$value) vs. when you use its value (e.g. x$value()). Just remember that when passing an argument to a module, you want the module to react to the value changing which means that you have to pass the reactive, not it’s current value.

  • If you find yourself frequently returning multiple values from a reactive, you might also consider using the zeallot package. zeallot provides the %<-% operator which allows you to assign into multiple variables (sometimes called multiple, unpacking, or destructuring assignment). This can be useful when returning multiple values because you avoid a layer of indirection.

    histogramApp <- function() {
      ui <- fluidPage(...)
      server <- function(input, output, session) {
        data <- datasetServer("data")
        c(value, name) %<-% selectVarServer("var", data)
        histogramServer("hist", value, name)
      shinyApp(ui, server)
  • The return value of a module should always be a reactive or, if you want to return multiple values, a list of reactives.

  • Another important use of modules is to give complex UI elements a simpler user interface.

19 Packages

  • The core idea of a package is that it’s a set of conventions for organising your code and related artefacts: if you follow those conventions, you get a bunch of tools for free.

  • In fact, all a project needs to be a package is a directory of R files and a DESCRIPTION file. A package is just a lightweight set of conventions that unlock useful tools and workflows.

20 Testing

  • As your app gets more complicated it becomes impossible to hold it all in your head simultaneously. Testing is a way to capture desired behaviour of your code, in such a way that you can automatically verify that it keeps working the way you expect. Turning your existing informal tests into code is painful when you first do it, because you need to carefully turn every key press and mouse click into a line of code, but once done, it’s tremendously faster to re-run your tests.

  • These levels of testing form a natural hierarchy because each technique provides a fuller simulation of the user experience of an app. The downside of the better simulations is that each level is slower because it has to do more, and more fragile because more external forces come into play. You should always strive to work at the lowest possible level so your tests are as fast and robust as possible. Over time this will also influence the way you write code: knowing what sort of code is easier to test will naturally push you towards simpler designs.

  • The art of testing is figuring out how to write tests that clearly define the expected behaviour of your function, without depending on incidental details that might change in the future.

  • Note that the second argument to expect_error() is a regular expression — the goal is to find a short fragment of text that matches the error you expect and is unlikely to match errors that you don’t expect.

  • A snapshot expectation differs from other expectations primarily in that the expected result is stored in a separate snapshot file, rather than in the code itself. Snapshot tests are most useful when you are designing complex user interface design systems, which is outside of the scope of most apps.

  • It’s very useful to verify that your tests test what you think they’re testing. A great way to do this is with “code coverage” which runs your tests and tracks every line of code that is run. You can then look at the results to see which lines of your code are never touched by a test, and gives you the opportunity to reflect on if you’ve tested the most important, highest risk, or hardest to program parts of your code. It’s not a substitute for thinking about your code — you can have 100% test coverage and still have bugs. But it’s a fun and a useful tool to help you think about what’s important, particularly when you have complex nested code.

  • When should you write tests? There are three basic options:

    • Before you write the code. This is a style of code called test driven development, and if you know exactly how a function should behave, it makes sense to capture that knowledge as code before you start writing the implementation.

    • After you write the code. While writing code you’ll often build up a mental to-do list of worries about your code. After you’ve written the function, turn these into tests so that you can be confident that the function works the way that you expect.

      When you start writing tests, beware writing them too soon. If your function is still actively evolving, keeping your tests up to date with all the changes is going to feel frustrating. That may indicate you need to wait a little longer.

    • When you find a bug. Whenever you find a bug, it’s good practice to turn it into an automated test case. This has two advantages. Firstly, to make a good test case, you’ll need to relentlessly simplify the problem until you have a very minimal reprex that you can include in a test. Secondly, you’ll make sure that the bug never comes back again!

21 Security

  • When securing your app, there are two main things to protect:

    • Your data: you want to make sure an attacker can’t access any sensitive data.

    • Your compute resources: you want to make sure an attacker can’t mine bitcoin or use your server as part of a spam farm.

  • Note that code within server() is isolated so there’s no way for one user session to see data from another. The only exception is if you use caching

  • Finally, note that Shiny inputs use client-side validation, i.e. the checks for valid input are performed by JavaScript in the browser, not by R. This means it’s possible for a knowledgeable attacker to send values that you don’t expect.

  • In general, the combination of parse() and eval() is a big warning sign for any Shiny app: they instantly make your app vulnerable. Similarly, you should never source() an uploaded .R file, or rmarkdown::render() an uploaded .Rmd. But these cases are pretty obvious, and are unlikely to be source of real problems.

    The bigger challenge arises because are a number of functions that parse(), eval(), or both, in a way that you’re not aware of.

22 Performance

  • This is a feature of Shiny: it allows you to quickly prototype a proof of concept that works for you, before figuring out how to make it fast so many people can use it simultaneously. Fortunately, it’s generally straightforward to get 10-100x performance with a few simple tweaks.

  • Benchmarking lets you check the performance of your app with multiple users, without actually exposing real people to a potentially slow app. Or if you want to serve 100s or 1000s of users, benchmarking will help you figure out just how many users each process can handle, and hence how many servers you’ll need to use.

  • As well as a flame graph, profvis also does its best to find and display the underlying source code so that you can click on a function in the flame graph to see exactly what’s run.

  • The most important limitation of profiling is due to the way it works: R has to stop the process and inspect what R functions are currently run. That means that R has to be in control.

  • Begin by resolving any issues where existing code is run more often than you expect — make sure you’re not repeating the same work in multiple reactives and that the reactive graph isn’t updating more often than you expect

  • Caching is a very powerful technique for improving code performance. The basic idea is to record the inputs to and outputs from every call to a function. When the cache function is called with a set of inputs that it’s already seen, it can replay the recorded output without recomputing.

Back to top