mcpField
S
shiny-r-optimize

shiny-r-optimize

github.com/posit-dev开发55152 次安装今天更新
代码仓库

关于 shiny-r-optimize

用于诊断和加速现有 Shiny for R 应用的技能,同时保持应用行为不变。它优先消除无用计算并复用结果,再考虑更快的数据访问或异步任务。

主要功能

  1. 01检查响应式与渲染瓶颈
  2. 02通过共享响应式减少重复计算
  3. 03使用缓存和输入门控
  4. 04改进数据加载与渲染
  5. 05测量改动并保持行为一致

适用场景

  1. 01缩短页面初次加载时间
  2. 02减少输入变化后的重复计算
  3. 03长时间操作期间保持应用响应
  4. 04比较代码改动前后的性能

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/posit-dev/skills --skill shiny-r-optimize

安装后在 Claude Code 中输入/shiny-r-optimize确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
async-tasks.md
caching.md
data-loading.md
diagnosis.md
reactive-graph.md
rendering-ui.md

详细介绍

优化 Shiny 应用性能

在不改变现有 Shiny for R 应用功能的前提下,让它运行得更快。典型情况是:应用原本供一两名开发者使用时运行良好,但现在速度太慢,或需要支持更多用户。保留所有应用逻辑;优先修改代码,而非调整部署;避免重写。

每项修复的优先顺序:

  1. 消除不必要的工作(不计算没人查看的输出;输入尚未就绪时不进行计算)。
  2. 复用重复的工作(共享响应式表达式、缓存)。
  3. 加速无法避免的工作(更快的数据格式、向量化代码)。
  4. 迁移会造成阻塞的工作(异步/后台任务)——最后再考虑;这只能提高并发能力,不能提升原始速度。

工作流程

1. 了解问题

明确“慢”具体指什么,并将其写成一个需要超越的基准:

  • 什么操作感觉慢? 首次加载页面?每次交互?某个特定操作?只有多人同时使用时才慢?
  • 需要支持多少并发用户?(1–5 人、约 20 人、数百人?)
  • 应用运行在哪里? 本地、Posit Connect、Posit Connect Cloud、Shiny Server?
  • 数据有多大、存放在哪里、多常更新?
2. 扫描源代码,查找常见问题并立即修复

阅读应用代码(app.R / server.R / ui.R / modules),查找典型问题。本表中的每项问题都有安全且不改变行为的修复方法:发现后立即修复,再进行测量。这十项改动可以解决大多数 Shiny 应用的性能问题。

问题(搜索以下内容)影响修复方法
在 server() 或 render 函数内部调用 read.csv/read_csv/DB 查询每个会话或每次失效都会重新运行在全局作用域中加载一次 → data-loading.md
多个输出中重复进行相同的子集筛选/汇总每次变化都会重复计算提取为一个共享的 reactive() → reactive-graph.md
在 observeEvent()/observe()内部定义渲染器重新创建输出;与响应式模型冲突在 server 顶层定义一次,并用 bindEvent()/req() 控制 → reactive-graph.md
耗时的 render* 没有 req() 保护启动时会对空值/尚未就绪的输入进行计算在函数体开头调用 req() → reactive-graph.md
首屏中有许多耗时的输出必须全部计算完成,页面才能使用移至标签页/导航中——隐藏的输出不会计算 → rendering-ui.md
重复调用相同的耗时操作,但没有使用 bindCache()每个人都要重复付出相同的计算成本使用 bindCache() → caching.md
滑块/文本输入直接连接耗时的计算链拖动一次就会触发数十次重新计算使用 debounce()/throttle()/bindEvent() → reactive-graph.md
在响应式表达式/渲染器中直接调用 Sys.sleep、长时间拟合模型、缓慢的 API/DB 调用阻塞整个 R 进程,影响所有用户使用 ExtendedTask → async-tasks.md
用 renderUI() 进行常规值更新每次更新都会重建 DOM 并重新绑定输入使用 update*Input() → rendering-ui.md
在 eventReactive/observeEvent 中读取范围过宽且没有使用 isolate()失效范围超出必要范围缩小依赖范围 → reactive-graph.md

按以下大致顺序应用修复(先处理成本最低的):将数据加载移至全局作用域 → 添加 req() 保护 → 共享响应式表达式 → 将耗时输出移至标签页 → 使用 bindCache() → 对频繁触发的输入进行 debounce → 用 update*Input() 替代 renderUI → 使用更快的数据格式(fread/feather/DuckDB)→ 使用 ExtendedTask 处理耗时数秒的操作 → 为剩余等待添加忙碌指示器(bslib::useBusyIndicators())。

3. 衡量——用于验证,而非设限

衡量是为了验证修复,并发现扫描无法发现的问题。 绝不能让衡量挤占实际行动。 如果应用难以运行、性能分析很慢,或结果不明确,也要落实扫描发现的问题,并说明哪些内容无法验证。

# Where does the time go? (CPU/memory profile)
p <- profvis::profvis(shiny::runApp("app.R"))
debrief::pv_print_debrief(p)   # debrief turns the profvis output into text
                               # summaries — hot functions/lines, call paths,
                               # memory, suggestions — much easier to act on
                               # than the interactive flame graph. Follow up
                               # with pv_focus(), pv_hot_lines(),
                               # pv_suggestions(); compare before/after runs
                               # with pv_print_compare().

# Is implementation A faster than B?
bench::mark(A(x), B(x), check = FALSE)

生态系统中的两个工具是供用户使用的,而不是让你运行的:reactlog 会让用户与自己的应用交互,并探索响应式图的可视化效果,以了解各响应式对象如何交互——通常,阅读代码并自行还原响应式图,就能以更低的成本获得相同甚至更多的见解。shinyloadtest 用于对已部署的应用进行负载测试,在优化结束时运行;当用户想确认基础设施能否承载计划支持的用户数量时,才使用它。

工具选择、如何解读每种工具的输出,以及如何解读负载测试结果: diagnosis.md。使用 profvis/debrief 查明“哪些代码运行缓慢”,使用 bench 比较两种实现;reactlog 留给用户,shinyloadtest 则用于评估已部署应用的承载能力。

4. 诊断扫描未发现的问题
症状最可能的原因从这里开始
每次会话首次加载页面都很慢在 server() 中加载/准备数据;启动时基于空输入计算输出;每个会话都进行大量设置data-loading.md、reactive-graph.md
一次交互导致过多内容重新计算依赖范围过宽;重复计算;没有缓存;输入频繁变动reactive-graph.md、caching.md
某项操作需要数秒,且应用会冻结刷新周期中有耗时的同步操作async-tasks.md
单个用户使用时很快,多个用户使用时性能下降阻塞操作占用进程;重复执行相同计算;每位用户都重新加载caching.md、async-tasks.md、diagnosis.md
图表/表格显示或更新缓慢渲染开销大;每次都完整重绘;大型数据使用 server = FALSE 的 DTrendering-ui.md
下载/上传行为异常或导致重新计算downloadHandler 重新计算;maxRequestSize 限制data-loading.md
仅在部署后变慢每个进程的全局变量和缓存;调度器设置diagnosis.md
5. 修复问题,先从改动成本最低的着手

按照已诊断的类别逐级处理;测量结果表明速度足够时就停止。每一级都能保持应用行为不变。

响应式重新计算 → reactive-graph.md

  1. 使用 req() 守卫——输入未就绪时不进行任何计算。
  2. 使用共享的 reactive()——每个派生值只计算一次。
  3. 缩小依赖范围:在更新循环中使用 bindEvent()/eventReactive()、isolate()、freezeReactiveValue()。
  4. 对频繁变动的输入使用 debounce()/throttle();如果更新会累积,则用 bslib::input_task_button() 将批量更新设为门控(可直接替代 actionButton())。
  5. 修复定时器:使用 on.exit(invalidateLater(...)),并使用开销较低的 checkFunc 配置 reactivePoll()。

重复执行相同工作 → caching.md

  1. 使用 bindCache() 缓存开销大的响应式表达式或渲染器——键必须涵盖函数体中读取的所有响应式值。
  2. 如果下游工作能从中受益,优先缓存数据,而不是图表。
  3. 使用 memoise() 缓存开销大的纯函数(数据库查询、模型拟合)。
  4. 谨慎选择缓存范围:"app" 可供多个用户共享(注意信息泄露),"session" 适用于用户专属数据,cachem::cache_disk() 可跨进程和重启共享。

阻塞操作 → async-tasks.md

  1. 只有在排除问题和缓存手段都已用尽后,才使用 ExtendedTask 搭配 mirai()(或 future_promise()),并配合 bind_task_button()。
  2. 必须配置工作进程后端——设置 mirai daemons() 或 future::plan(multisession);如果使用 future 默认的 plan(sequential),所有操作仍会阻塞。
  3. 不要在工作进程中读取响应式值;通过 invoke() 传递值。

数据访问 → data-loading.md

  1. 将数据加载到全局作用域;不要在渲染函数中读取数据。
  2. 使用 fread/read_csv(由 vroom 驱动),而不是 read.csv;使用 feather/qs/fst,而不是在 CSV/RDS 之间来回转换。
  3. 当数据超出 RAM 容量或只需读取部分数据时,使用 DuckDB + parquet,或使用 pool + dbplyr 下推计算。

渲染与 UI → rendering-ui.md

  1. 将开销大的输出放在选项卡/导航项中,避免计算隐藏的输出。
  2. 使用按钮控制推测性输出(req() + bindEvent())。
  3. 对 DT 使用服务器端处理 + replaceData();对于大型 plotly 图表,使用 WebGL/资源包;工作运行期间显示繁忙指示器。
6. 验证并迭代

每次只做一项更改,然后重新测量并与基准数据比较。保持可观察到的行为完全一致——删掉功能的“更快”应用并不算更快。完成前,从原始应用和修改后的应用中(或从原始数据中)重新计算一个有代表性的输出值,并确认两者一致——这样能以较低成本发现静默的行为偏差。

报告完整性:将所有引用的测量结果(脚本输出、计时日志)保存在应用项目的 outputs/measurements/ 下——如果文件夹不存在,就创建它——并根据保存的材料核对最终报告中的每个数字和机制说明。如果某项陈述无法通过材料验证,请在报告中说明,而不要直接断言。

<app-project>/
  outputs/
    measurements/    # the audit trail for the final report: one file per
                     # measurement (profvis output, bench results, timings),
                     # named for the fix or code path it measured

基本原则

  • 显而易见的修改批量处理;不确定的修改单独处理。 对效果可预测的安全修复——将数据加载移至全局作用域、整合重复的筛选操作、替换 renderUI——可以一并实施,再统一重新测量。只有在确实无法确定哪项修改起了作用,或修改未达到预期效果时,才逐项修改;只有这种情况下,追溯效果来源才值得额外进行多轮测量。
  • 保留原有行为。 UI 修改仅限于改善感知性能的辅助功能(加载指示器、任务按钮、标签页),并且必须告知用户——改变时序语义的修改也同样如此,即使输出相同也要告知(debounce() 会增加有意延迟;bindCache()/memoise() 会在不同会话间共享结果)。
  • 优先使用现代 API:优先选用 bindCache()/bindEvent(),而不是 renderCachedPlot() 和原始的 eventReactive 组合;优先选用 ExtendedTask,而不是手动编写 promise。只有当应用已在使用旧版等效方案时,才提及它们。
  • 先缓存,再异步处理。 缓存通常能带来 10–100 倍的提升;异步处理只会增加并发能力,同时带来序列化和调试成本。
  • 添加依赖项前先征求同意(duckdb、data.table、mirai……)。

参考资料

仅在需要时阅读诊断结果所指向的参考资料——不要一开始就把所有资料都读完。

  • diagnosis.md — profvis/debrief、bench 和负载测试(shinyloadtest/shinycannon)及阅读指南;多用户进程模型;Connect 和 Posit Connect Cloud 容量配置项。
  • reactive-graph.md — 共享的响应式对象、req()、isolate()、bindEvent()、freezeReactiveValue()、debounce()/throttle()、计时器和依赖风暴修复方法。
  • caching.md — bindCache() 键规则与作用范围、缓存后端、memoise()、缓存前检查清单。
  • async-tasks.md — 阻塞为何有害、ExtendedTask/mirai/future_promise 模式及其硬性规则。
  • data-loading.md — 仅加载一次的模式、文件格式基准测试、DuckDB/parquet、pool/dbplyr、下载和上传。
  • rendering-ui.md — 通过标签页暂停输出、renderUI 的替代方案、图表/表格渲染成本、感知性能。

评论

支持 Markdown