You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

优化Shiny应用侧边栏选项切换时的响应速度

Shiny应用侧边栏切换响应缓慢的优化方案

问题背景

开发的Shiny应用在切换侧边栏的地理单元、区域/郊区或类别选项时,响应延迟达8-15秒。问题根源在于每次切换都需要处理并渲染大量数据,示例代码因数据集较小无法复现,但大数据集下延迟问题显著。

现有代码的核心问题

  1. 冗余计算:当前的memoise函数(如Cate1_plotsRegion)会一次性生成所有categories对应的全部图表,但实际每次仅需要展示一个,造成大量不必要的计算开销。
  2. 不必要的依赖触发:renderPlot中依赖了所有可能的输入(如input$EN_Cate_AB和input$EN_Cate_C同时存在),导致无关输入变化也会触发重计算。
  3. 重复过滤逻辑:多个函数重复执行类似的数据过滤操作,没有统一复用逻辑。
  4. 静态数据预过滤:提前生成Cate1_region等静态数据框,无法利用Shiny的响应式缓存机制动态适配输入变化。

优化方案

1. 精准缓存,按需计算图表

修改缓存函数,仅针对当前需要的类别生成对应图表,避免一次性计算所有可能的图表。

2. 精简响应式依赖

在renderPlot中仅依赖当前场景下需要的输入,避免无关输入触发重计算。

3. 统一数据过滤逻辑

用响应式函数根据输入动态过滤数据,减少重复代码,提升过滤效率。

4. 优化ggplot性能

  • 提前处理数据的因子水平,避免在绘图函数中重复转换
  • 定义统一的主题对象,减少重复的theme()设置

优化后的完整代码

示例数据

Data <- data.frame(
  Geographic_unit = c(rep("Region",16), rep("Suburb",16)),
  Areaname = c(rep("Region1",8), rep("Region2",8), rep("Suburb1",8), rep("Suburb2",8)),
  Category = rep(c(rep("Cate1",4), rep("Cate2",4)),4),
  Breakdown = rep(c(rep("Sex",2), rep("Year",2), rep("Sex",2), rep("Age group",2)),4),
  Breakdown_cate = rep(c("male","female",1990,2019,"male","female",">50","<50"),4),
  Number = runif(32, min=20, max=200)
)

Shiny应用代码

library(shiny)
library(ggplot2)  
library(dplyr)  
library(memoise)
library(glue)

# 全局配置
pcolors1 <- c("#8DA0CB", "#FEE090")
pcolors2 <- c("#66C2A5", "#FC8D62", "#8DA0CB", "#FEE090")
levels_pie2cates <- list(c("female","male"), c("<50",">50"))

# 统一主题定义
theme_custom_pie <- theme_void() +
  theme(
    legend.title = element_blank(),
    legend.text = element_text(size=16),
    legend.key.size = unit(1, "cm"),
    plot.title = element_text(size=18, face="bold", hjust=0.5, vjust=-5)
  )

theme_custom_line <- theme_minimal() +
  theme(
    plot.title = element_text(size=18, face="bold", hjust=0.5, vjust=-1),
    axis.title.x = element_text(size=16),
    axis.title.y = element_blank(),
    axis.text = element_text(size=12),
    legend.title = element_blank(),
    legend.key.size = unit(0.8, "cm"),
    legend.position = "none"
  )

# 绘图函数:只做绘图逻辑,数据提前处理
pie_plot <- function(data, category, colors, levels) {
  ggplot(data, aes(x="", y=Number, fill=Breakdown_cate)) +
    geom_bar(stat="identity", width=1, color="white") +
    coord_polar("y", start=50) +
    scale_fill_manual(values=colors) +
    scale_x_discrete(limits=levels) +
    ggtitle(category) +
    theme_custom_pie
}

line_plot <- function(data, category) {
  ggplot(data, aes(x=Breakdown_cate, y=Number, group=Areaname)) +
    geom_line(color="#378892", size=2, alpha=0.9, linetype=1) +
    geom_point(size=4, color="#1E656D") +
    xlab("Years") +
    ggtitle(category) +
    theme_custom_line
}

# 缓存函数:仅生成当前需要的图表,而非全部
memoised_plot <- memoise(function(geographic_unit, category, subarea, breakdown) {
  # 动态过滤数据
  filtered_data <- Data %>%
    filter(
      Geographic_unit == geographic_unit,
      Category == category,
      Areaname == subarea,
      Breakdown == breakdown
    ) %>%
    mutate(Breakdown_cate = factor(Breakdown_cate, levels = switch(breakdown,
      "Sex" = levels_pie2cates[[1]],
      "Age group" = levels_pie2cates[[2]],
      as.character(unique(.$Breakdown_cate))
    )))
  
  # 根据breakdown选择绘图函数
  switch(breakdown,
    "Sex" = pie_plot(filtered_data, breakdown, pcolors1, levels_pie2cates[[1]]),
    "Year" = line_plot(filtered_data, breakdown),
    "Age group" = pie_plot(filtered_data, breakdown, pcolors2, levels_pie2cates[[2]])
  )
})

# UI
ui <- fluidPage(
  navbarPage(title = "",
             tabPanel("EN",
                      fluidRow(column(12, p(h1(textOutput("selected_areaEN"))))),
                      br(),
                      sidebarPanel(
                        selectInput("EN_Area", label = "Geographical units",
                                    c("Region" = "A","Suburb" = "B"),
                                    selected = "A"),
                        width = 3),
                      sidebarPanel( 
                        conditionalPanel(
                          condition = "input.EN_Area == 'A'",
                          selectInput("EN_Area_Region", label = "Region",
                                      c("Region1","Region2"),
                                      selected = "Region1")),
                        conditionalPanel(
                          condition = "input.EN_Area == 'B'",
                          selectInput("EN_Area_Sub", label = "Suburb",
                                      c("Suburb1", "Suburb2"),
                                      selected = "Suburb1"))),
                      sidebarPanel(
                        selectInput("Cate_option_EN", label = "Categories",
                                    c("Cate1","Cate2"),
                                    selected = "Cate1"),
                        width = 3),
                      mainPanel(
                        tabsetPanel(
                          tabPanel("Absolute number (%)", plotOutput("ENver_Abn")),
                          tabPanel("Rate per 100,000 person-years", plotOutput("ENver_Rate"))
                        ),
                        width = 9),
                      sidebarPanel(
                        conditionalPanel(
                          condition = "input.Cate_option_EN == 'Cate1'",
                          radioButtons("EN_Cate_AB", label = "Breakdown",
                                       c("Sex","Year"), selected = "Sex")),
                        conditionalPanel(
                          condition = "input.Cate_option_EN == 'Cate2'",
                          radioButtons("EN_Cate_C", label = "Breakdown",
                                       c("Sex","Age group"), selected = "Sex")),
                        width = 3)
             )
  )
)

# Server
server <- function(input, output) {
  # 动态获取当前子区域
  current_subarea <- reactive({
    switch(input$EN_Area,
           "A" = input$EN_Area_Region,
           "B" = input$EN_Area_Sub)
  })
  
  # 动态获取当前breakdown选项
  current_breakdown <- reactive({
    switch(input$Cate_option_EN,
           "Cate1" = input$EN_Cate_AB,
           "Cate2" = input$EN_Cate_C)
  })
  
  # 标题文本
  output$selected_areaEN <- renderText({
    glue("{input$Cate_option_EN} in {switch(input$EN_Area,
           'A' = input$EN_Area_Region,
           'B' = paste('the ', input$EN_Area_Sub)
         )}")
  })
  
  # 渲染图表:仅依赖必要的输入
  output$ENver_Abn <- renderPlot({
    req(current_subarea(), current_breakdown(), input$Cate_option_EN, input$EN_Area)
    
    # 转换地理单元名称
    geographic_unit <- switch(input$EN_Area,
                              "A" = "Region",
                              "B" = "Suburb")
    
    # 调用缓存函数生成图表
    memoised_plot(geographic_unit, input$Cate_option_EN, current_subarea(), current_breakdown())
  }) %>% bindCache(input$EN_Area, input$Cate_option_EN, current_subarea(), current_breakdown())
}

shinyApp(ui, server)

额外优化建议

  • 如果数据量极大,可考虑提前预处理数据,将常用的过滤结果保存为RDS文件,启动时加载,减少实时计算量。
  • 启用shiny::enableBookmarking()可以缓存用户的输入状态,但需注意数据隐私。
  • 对于极复杂的图表,可考虑用plotly替代ggplot2,plotly的渲染性能在某些场景下更优,且支持交互。

内容的提问来源于stack exchange,提问作者user22137965

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 10:57:00