optimize-shiny-performance
Profile and optimize Shiny application performance using profvis, bindCache, memoise, async/promises, debounce/throttle, and ExtendedTask for long-running computations. Use when the app feels slow or unresponsive during user interaction, when server resources are exhausted under concurrent load, when specific operations create bottlenecks, or when preparing an app for production deployment with many concurrent users.
Best use case
optimize-shiny-performance is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Profile and optimize Shiny application performance using profvis, bindCache, memoise, async/promises, debounce/throttle, and ExtendedTask for long-running computations. Use when the app feels slow or unresponsive during user interaction, when server resources are exhausted under concurrent load, when specific operations create bottlenecks, or when preparing an app for production deployment with many concurrent users.
Teams using optimize-shiny-performance should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/optimize-shiny-performance/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How optimize-shiny-performance Compares
| Feature / Agent | optimize-shiny-performance | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Profile and optimize Shiny application performance using profvis, bindCache, memoise, async/promises, debounce/throttle, and ExtendedTask for long-running computations. Use when the app feels slow or unresponsive during user interaction, when server resources are exhausted under concurrent load, when specific operations create bottlenecks, or when preparing an app for production deployment with many concurrent users.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Optimize Shiny Performance
Profile, diagnose, and optimize Shiny application performance through caching, async operations, and reactive graph optimization.
## When to Use
- Shiny app feels slow or unresponsive during user interaction
- Server resources are exhausted under concurrent user load
- Specific operations (data loading, plotting, computation) create bottlenecks
- Preparing an app for production deployment with many users
## Inputs
- **Required**: Path to the Shiny application
- **Required**: Description of the performance problem (slow load, laggy interaction, high memory)
- **Optional**: Number of expected concurrent users
- **Optional**: Available server resources (RAM, CPU cores)
- **Optional**: Whether the app uses a database or external API
## Procedure
### Step 1: Profile the Application
```r
# Profile with profvis
profvis::profvis({
shiny::runApp("path/to/app", display.mode = "normal")
})
# Or profile specific operations
profvis::profvis({
result <- expensive_computation(data)
})
```
Identify the top bottlenecks:
1. **Data loading**: How long does initial data fetch take?
2. **Reactive recalculation**: Which reactives fire most often?
3. **Rendering**: Which outputs take the longest to render?
4. **External calls**: Database queries, API requests, file I/O?
Use the reactive log for reactive graph analysis:
```r
# Enable reactive logging
options(shiny.reactlog = TRUE)
shiny::runApp("path/to/app")
# Press Ctrl+F3 in the browser to view the reactive graph
```
**Got:** Clear identification of the 2-3 biggest bottlenecks.
**If fail:** If profvis does not show useful detail, wrap specific sections with `profvis::profvis()`. If reactlog is overwhelming, focus on one interaction at a time.
### Step 2: Optimize Reactive Graph
Reduce unnecessary reactive invalidations:
```r
# BAD: Recomputes on ANY input change
output$plot <- renderPlot({
data <- load_data() # Runs every time
filtered <- data[data$category == input$category, ]
plot(filtered)
})
# GOOD: Isolate data loading from filtering
raw_data <- reactive({
load_data()
}) |> bindCache() # Cache the expensive part
filtered_data <- reactive({
raw_data()[raw_data()$category == input$category, ]
})
output$plot <- renderPlot({
plot(filtered_data())
})
```
Use `isolate()` to prevent unnecessary invalidations:
```r
# Only recompute when the button is clicked, not on every input change
output$result <- renderText({
input$compute # Take dependency on button
isolate({
paste("N =", input$n, "Mean =", mean(rnorm(input$n)))
})
})
```
Use `debounce()` and `throttle()` for high-frequency inputs:
```r
# Debounce text input — wait 500ms after user stops typing
search_text <- reactive(input$search) |> debounce(500)
# Throttle slider — update at most every 250ms
slider_value <- reactive(input$slider) |> throttle(250)
```
**Got:** Reactive graph fires only necessary recalculations.
**If fail:** If removing a dependency breaks functionality, use `req()` to add explicit guards instead of relying on implicit reactive dependencies.
### Step 3: Implement Caching
#### bindCache for Shiny Outputs
```r
output$plot <- renderPlot({
create_expensive_plot(filtered_data())
}) |> bindCache(input$category, input$date_range)
output$table <- renderDT({
expensive_query(input$filters)
}) |> bindCache(input$filters)
```
`bindCache` uses input values as cache keys. When the same inputs occur again, the cached result is returned immediately.
#### memoise for Functions
```r
# Cache expensive function results
load_reference_data <- memoise::memoise(
function(dataset_name) {
readr::read_csv(paste0("data/", dataset_name, ".csv"))
},
cache = cachem::cache_disk("cache/", max_age = 3600)
)
```
#### App-level Data Pre-computation
```r
# In global.R or outside server function — computed once at app startup
reference_data <- readr::read_csv("data/reference.csv")
model <- readRDS("models/trained_model.rds")
server <- function(input, output, session) {
# reference_data and model are available to all sessions
# without reloading
}
```
**Got:** Repeated operations use cached results; response time drops significantly.
**If fail:** If cache grows too large, set `max_age` or `max_size` limits. If cached values are stale, reduce `max_age` or add a cache-clear button. If `bindCache` causes errors, ensure cache key inputs are serializable.
### Step 4: Add Async for Long Operations
Use `ExtendedTask` (Shiny >= 1.8.1) for long-running computations:
```r
server <- function(input, output, session) {
# Define the extended task
analysis_task <- ExtendedTask$new(function(data, params) {
promises::future_promise({
# This runs in a background process
run_heavy_analysis(data, params)
})
}) |> bind_task_button("run_analysis")
# Trigger the task
observeEvent(input$run_analysis, {
analysis_task$invoke(dataset(), input$params)
})
# Use the result
output$result <- renderTable({
analysis_task$result()
})
}
```
For apps on Shiny < 1.8.1, use promises directly:
```r
library(promises)
library(future)
plan(multisession, workers = 4)
server <- function(input, output, session) {
result <- eventReactive(input$compute, {
future_promise({
Sys.sleep(5) # Simulate long computation
expensive_analysis(isolate(input$params))
})
})
output$table <- renderTable({
result()
})
}
```
**Got:** Long operations do not block the UI; other users can interact while computation runs.
**If fail:** If `future_promise` errors, check that `plan(multisession)` is set. If variables are not available in the future, pass them explicitly — futures run in separate R processes.
### Step 5: Optimize Rendering
Reduce rendering overhead:
```r
# Use plotly for interactive plots instead of re-rendering
output$plot <- plotly::renderPlotly({
plotly::plot_ly(filtered_data(), x = ~x, y = ~y, type = "scatter")
})
# Use server-side DT for large tables
output$table <- DT::renderDataTable({
DT::datatable(large_data(), server = TRUE, options = list(
pageLength = 25,
processing = TRUE
))
})
# Conditional UI to avoid rendering hidden elements
output$details <- renderUI({
req(input$show_details)
expensive_details_ui()
})
```
**Got:** Rendering operations are faster and do not block the UI.
**If fail:** If plotly is slow with large datasets, use `toWebGL()` for WebGL rendering or downsample data before plotting.
### Step 6: Validate Performance Improvements
```r
# Before/after benchmarking
system.time({
shiny::testServer(myModuleServer, args = list(...), {
session$setInputs(category = "A")
session$flushReact()
})
})
# Load testing with shinyloadtest
shinyloadtest::record_session("http://localhost:3838")
shinyloadtest::shinycannon(
"recording.log",
"http://localhost:3838",
workers = 10,
loaded_duration_minutes = 5
)
shinyloadtest::shinyloadtest_report("recording.log")
```
**Got:** Measurable improvement in response times and/or concurrent user capacity.
**If fail:** If performance did not improve, re-profile to find the next bottleneck. Performance optimization is iterative — fix the biggest bottleneck first, then re-measure.
## Validation
- [ ] Profiling identifies specific bottlenecks (not guessing)
- [ ] Reactive graph has no unnecessary invalidation chains
- [ ] Expensive operations use caching (bindCache or memoise)
- [ ] Long-running computations use async (ExtendedTask or promises)
- [ ] High-frequency inputs use debounce/throttle
- [ ] Large datasets use server-side processing
- [ ] Performance improvement is measurable (before/after timing)
## Pitfalls
- **Premature optimization**: Profile first. The bottleneck is rarely where you think it is.
- **Cache invalidation bugs**: If users see stale data, the cache key does not include all relevant inputs. Add missing dependencies to `bindCache()`.
- **Future variable scoping**: `future_promise` runs in a separate process. Global variables, database connections, and reactive values must be captured explicitly.
- **Reactive spaghetti**: If the reactive graph is too complex to understand, the app needs architectural refactoring (modules), not just caching.
- **Over-caching**: Caching everything wastes memory. Only cache operations that are expensive AND have repeated input patterns.
## Related Skills
- `build-shiny-module` — modular architecture for maintainable reactive code
- `scaffold-shiny-app` — choose the right app framework from the start
- `deploy-shiny-app` — deploy optimized apps with appropriate server resources
- `test-shiny-app` — performance regression testsRelated Skills
scaffold-shiny-app
Scaffold a new Shiny application using golem (production R package), rhino (enterprise), or vanilla (quick prototype) structure. Covers framework selection, project initialization, and first module creation. Use to start a new interactive web app in R, create a dashboard or data explorer prototype, set up a production Shiny app as an R package with golem, or bootstrap an enterprise Shiny project with rhino.
optimize-docker-build-cache
Optimize Docker build times using layer caching, multi-stage builds, BuildKit features, and dependency-first copy patterns. Applicable to R, Node.js, and Python projects. Use when Docker builds are slow due to repeated package installations, when rebuilds reinstall all dependencies on every code change, when image sizes are unnecessarily large, or when CI/CD pipeline builds are a bottleneck.
optimize-cloud-costs
Implement cloud cost optimization strategies for Kubernetes workloads using tools like Kubecost for visibility, right-sizing recommendations, horizontal and vertical pod autoscaling, spot/preemptible instances, and resource quotas. Covers cost allocation, showback reporting, and continuous optimization practices. Use when cloud costs are growing without proportional business value, when resource requests are misaligned with actual usage, when manual scaling leads to over-provisioning, or when implementing showback and chargeback for internal cost accountability.
design-shiny-ui
Design Shiny application UIs using bslib for theming, layout_columns for responsive grids, value boxes, cards, and custom CSS/SCSS. Covers page layouts, accessibility, and brand consistency. Use when building a new Shiny app UI from scratch, modernizing an existing app from fluidPage to bslib, applying brand theming, making a Shiny app responsive across screen sizes, or improving accessibility of a Shiny application.
deploy-shinyproxy
Deploy ShinyProxy for hosting multiple containerized Shiny applications. Covers ShinyProxy Docker deployment, application.yml configuration, Shiny app Docker images, authentication, container backends, usage tracking, and scaling. Use when hosting multiple Shiny apps behind a single entry point, needing per-app authentication and access control, deploying Shiny apps as isolated Docker containers, or scaling beyond single-app deployment with usage analytics and audit logging.
deploy-shiny-app
Deploy Shiny applications to shinyapps.io, Posit Connect, or Docker containers. Covers rsconnect configuration, manifest generation, Dockerfile creation, and deployment verification. Use when publishing a Shiny app for external or internal users, moving from local development to a hosted environment, containerizing a Shiny app for Kubernetes or Docker deployment, or setting up automated deployment pipelines.
build-shiny-module
Build reusable Shiny modules with proper namespace isolation using NS(). Covers module UI/server pairs, reactive return values, inter-module communication, and nested module composition. Use when extracting a reusable component from a growing Shiny app, building a UI widget used in multiple places, encapsulating complex reactive logic behind a clean interface, or composing larger applications from smaller, testable units.
skill-name-here
One to three sentences describing what this skill accomplishes, followed by key activation triggers. This field is the primary mechanism agents use to decide whether to activate the skill — it is read during discovery before the full body is loaded. Start with a verb. Include the most important "when to use" conditions inline. Max 1024 characters.
write-vignette
Create R package vignettes using R Markdown or Quarto. Covers vignette setup, YAML configuration, code chunk options, building and testing, and CRAN requirements for vignettes. Use when adding a Getting Started tutorial, documenting complex workflows spanning multiple functions, creating domain-specific guides, or when CRAN submission requires user-facing documentation beyond function help pages.
write-validation-documentation
Write IQ/OQ/PQ validation documentation for computerized systems in regulated environments. Covers protocols, reports, test scripts, deviation handling, and approval workflows. Use when validating R or other software for regulated use, preparing for a regulatory audit, documenting qualification of computing environments, or creating and updating validation protocols and reports for new or re-qualified systems.
write-testthat-tests
Write comprehensive testthat (edition 3) tests for R package functions. Covers test organization, expectations, fixtures, mocking, snapshot tests, parameterized tests, and achieving high coverage. Use when adding tests for new package functions, increasing test coverage for existing code, writing regression tests for bug fixes, or setting up test infrastructure for a package that lacks it.
write-standard-operating-procedure
Write a GxP-compliant Standard Operating Procedure (SOP). Covers regulatory SOP template structure (purpose, scope, definitions, responsibilities, procedure, references, revision history), approval workflow design, periodic review scheduling, and operational procedures for system use. Use when a new validated system requires operational procedures, when existing informal procedures need formalisation, when an audit finding cites missing procedures, when a change control triggers SOP updates, or when periodic review identifies outdated procedural content.