关于 shiny-r-optimize
用于诊断和加速现有 Shiny for R 应用的技能,同时保持应用行为不变。它优先消除无用计算并复用结果,再考虑更快的数据访问或异步任务。
主要功能
- 01检查响应式与渲染瓶颈
- 02通过共享响应式减少重复计算
- 03使用缓存和输入门控
- 04改进数据加载与渲染
- 05测量改动并保持行为一致
适用场景
- 01缩短页面初次加载时间
- 02减少输入变化后的重复计算
- 03长时间操作期间保持应用响应
- 04比较代码改动前后的性能
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ posit-dev/ skills --skill shiny-r-optimize安装后在 Claude Code 中输入/shiny-r-optimize确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/async-tasks.mdcaching.mddata-loading.mddiagnosis.mdreactive-graph.mdrendering-ui.md详细介绍
优化 Shiny 应用性能
在不改变现有 Shiny for R 应用功能的前提下,让它运行得更快。典型情况是:应用原本供一两名开发者使用时运行良好,但现在速度太慢,或需要支持更多用户。保留所有应用逻辑;优先修改代码,而非调整部署;避免重写。
每项修复的优先顺序:
- 消除不必要的工作(不计算没人查看的输出;输入尚未就绪时不进行计算)。
- 复用重复的工作(共享响应式表达式、缓存)。
- 加速无法避免的工作(更快的数据格式、向量化代码)。
- 迁移会造成阻塞的工作(异步/后台任务)——最后再考虑;这只能提高并发能力,不能提升原始速度。
工作流程
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 的 DT | rendering-ui.md |
| 下载/上传行为异常或导致重新计算 | downloadHandler 重新计算;maxRequestSize 限制 | data-loading.md |
| 仅在部署后变慢 | 每个进程的全局变量和缓存;调度器设置 | diagnosis.md |
5. 修复问题,先从改动成本最低的着手
按照已诊断的类别逐级处理;测量结果表明速度足够时就停止。每一级都能保持应用行为不变。
响应式重新计算 → reactive-graph.md
- 使用
req()守卫——输入未就绪时不进行任何计算。 - 使用共享的
reactive()——每个派生值只计算一次。 - 缩小依赖范围:在更新循环中使用
bindEvent()/eventReactive()、isolate()、freezeReactiveValue()。 - 对频繁变动的输入使用
debounce()/throttle();如果更新会累积,则用bslib::input_task_button()将批量更新设为门控(可直接替代actionButton())。 - 修复定时器:使用
on.exit(invalidateLater(...)),并使用开销较低的checkFunc配置reactivePoll()。
重复执行相同工作 → caching.md
- 使用
bindCache()缓存开销大的响应式表达式或渲染器——键必须涵盖函数体中读取的所有响应式值。 - 如果下游工作能从中受益,优先缓存数据,而不是图表。
- 使用
memoise()缓存开销大的纯函数(数据库查询、模型拟合)。 - 谨慎选择缓存范围:
"app"可供多个用户共享(注意信息泄露),"session"适用于用户专属数据,cachem::cache_disk()可跨进程和重启共享。
阻塞操作 → async-tasks.md
- 只有在排除问题和缓存手段都已用尽后,才使用
ExtendedTask搭配mirai()(或future_promise()),并配合bind_task_button()。 - 必须配置工作进程后端——设置 mirai
daemons()或future::plan(multisession);如果使用 future 默认的plan(sequential),所有操作仍会阻塞。 - 不要在工作进程中读取响应式值;通过
invoke()传递值。
数据访问 → data-loading.md
- 将数据加载到全局作用域;不要在渲染函数中读取数据。
- 使用
fread/read_csv(由 vroom 驱动),而不是read.csv;使用feather/qs/fst,而不是在 CSV/RDS 之间来回转换。 - 当数据超出 RAM 容量或只需读取部分数据时,使用 DuckDB + parquet,或使用
pool+dbplyr下推计算。
渲染与 UI → rendering-ui.md
- 将开销大的输出放在选项卡/导航项中,避免计算隐藏的输出。
- 使用按钮控制推测性输出(
req()+bindEvent())。 - 对 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的替代方案、图表/表格渲染成本、感知性能。