Skip to content

Add shiny-r-optimize skill for Shiny app performance - #91

Merged
gadenbuie merged 16 commits into
mainfrom
skill/shiny-optimize
Oct 14, 2026
Merged

gadenbuie merged 16 commits into
mainfrom
skill/shiny-optimize

Conversation

@gadenbuie

@gadenbuie gadenbuie commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds shiny/shiny-r-optimize — a skill that guides an app developer through finding and fixing performance bottlenecks in an existing Shiny for R app: the app that worked fine during prototyping but is now too slow, too fragile, or unable to support the number of users who actually need it.

Purpose

The skill walks an agent through a diagnosis-then-treatment workflow: understand the complaint → scan the source for classic performance smells and apply the safe, behavior-preserving fixes immediately → measure to confirm (profvis, reactlog, bench, shinyloadtest) → classify the bottleneck → apply the cheapest effective fix → verify with a parity check and saved measurements. Its core principle is "do less work before doing work faster": eliminate, reuse, accelerate, and only then relocate work off the main process. All fixes preserve app logic and behavior.

Use cases

  • "My app takes 15+ seconds to load and every slider move re-renders everything"
  • "When one user clicks Run, the whole app freezes for everyone" (blocking ops → ExtendedTask)
  • "It was fine with 2 users but dies with 10" (caching, async, capacity)
  • "Profile it and tell me the top 3 things making it slow" (profile-first triage)
  • Preparing a prototyped app for real-world traffic before deployment

Structure

SKILL.md (workflow, smell table, quick-wins scan, symptom→fix table, fix ladders) plus six reference files loaded on demand: diagnosis.md (profiling/load-testing, incl. the multi-process model and Connect/shinyapps.io knobs), reactive-graph.md, caching.md, async-tasks.md, data-loading.md, rendering-ui.md.

Dependencies

  • The skill itself has no dependencies; the techniques reference base + recommended packages as needed (profvis, reactlog, bench, shinyloadtest, memoise, cachem, future/promises/mirai, duckdb, pool/dbplyr, DT, bslib). The skill instructs the agent to ask before adding new dependencies to an app.
  • Examples assume Shiny ≥ 1.8.1 for ExtendedTask (≥ 1.6.0 for bindCache/bindEvent); version constraints are noted inline.

Documentation

  • Written for any agent (imperative, step-by-step), with a reference map that directs it to read only the reference its diagnosis points to.
  • Registered in the shiny plugin in .claude-plugin/marketplace.json; documented in shiny/README.md.

Verification

shiny-r-optimize (1,122 lines, ~12,542 tokens)

Skill description: ~99 tokens

File Lines ~Tokens
SKILL.md 194 2,869
references/async-tasks.md 165 1,770
references/caching.md 148 1,575
references/data-loading.md 148 1,578
references/diagnosis.md 166 1,705
references/reactive-graph.md 201 1,902
references/rendering-ui.md 100 1,143

Evals

Evaluated over two iterations with three realistic scenarios (broad-slowness dashboard, multi-user freezing, profile-first triage) against deliberately-broken fixture apps, each run graded against 6–10 objective assertions by an independent grader:

Iteration With skill Without skill Time (with vs without)
1 (research-dump draft) 75% 77% 360s vs 282s
2 (lean revision) 100% 95.8% 351s vs 827s (2.4× faster)

Iteration 2 also used ~22% fewer tokens than unassisted runs. Eval fixtures, prompts, assertions, per-run grades, and benchmark aggregates are preserved in shiny/shiny-r-optimize-workspace/ (not included in this PR).

  • Manual verification: fixture apps boot and serve HTTP 200 before and after modification; modified apps were verified with shinytest2/chromote during evals.

@gadenbuie
gadenbuie marked this pull request as ready for review October 6, 2026 23:40
@gadenbuie gadenbuie changed the title Add shiny-optimize skill for Shiny app performance Add shiny-r-optimize skill for Shiny app performance Oct 7, 2026
@gadenbuie

Copy link
Copy Markdown
Collaborator Author

@cpsievert @schloerke if you have a minute to give this a quick scan -- have I missed anything?

@cpsievert cpsievert left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, just minor comments

Comment thread shiny/shiny-r-optimize/SKILL.md Resolved
Comment thread shiny/shiny-r-optimize/SKILL.md Resolved
Comment thread shiny/shiny-r-optimize/SKILL.md Resolved
Comment thread shiny/README.md Resolved
@schloerke

Copy link
Copy Markdown
Contributor

Thank you!

Comprehensive performance-optimization skill for Shiny for R apps:
SKILL.md diagnostic workflow + 7 reference files distilled from the
Posit Conf 2024 Level-Up Shiny workshops, the official shiny.posit.co
performance articles, and broader ecosystem research (Mastering Shiny,
promises/mirai, Appsilon, shinyloadtest).

Intentionally oversized (~17k tokens) as a first draft; a review pass
will trim to a leaner, more prescriptive version.

- Register in shiny plugin
- Category README entry
- Eval workspace kept uncommitted (shiny/shiny-optimize-workspace/)
Review pass over the first draft (1,430 -> 1,097 lines, ~17.2k -> ~12.2k
tokens, -29%):

- Restructure workflow: apply safe behavior-preserving fixes from the
  smell scan immediately; measurement confirms rather than gates
  (addresses analysis-paralysis seen in eval iteration 1)
- Instruct reading only the reference the diagnosis points to
- Merge scaling-users.md into diagnosis.md (7 -> 6 reference files)
- De-duplicate flush-cycle explanation; cut prose padding, version
  trivia, and inline citations in favor of short further-reading lists
- Preserve verbatim: ExtendedTask pattern, bindCache key rules,
  bindCache|>bindEvent ordering, debounce/throttle semantics,
  suspendWhenHidden behavior, data-format benchmarks
- Fix stale scaling-users.md mention in category README
Eval findings (with-skill 100% vs 95.8% baseline, 2.4x faster, 22%
fewer tokens) and grader observations drove five surgical additions:

- Report integrity: cited measurements must be saved under
  outputs/measurements/ and claims checked against them; unverifiable
  claims are flagged, not asserted
- Preserve-behavior rule now flags timing-semantics changes
  (debounce latency, cross-session cache sharing)
- Parity check on one representative output before finishing
- caching.md: bindCache vs memoise vs hoist decision guide
- async-tasks.md: clarify shared $result() consumer semantics

Skill remains lean: 1,122 lines / ~12.5k tokens
Following the skill-creator convention, keep the eval workspace as a
sibling of the skill directory. Commit selectively:

- evals/evals.json: eval prompts and assertions for the three test cases
- apps/: deliberately-slow fixture apps (sales-dashboard, model-explorer)
  with READMEs documenting the planted performance problems
- .gitignore: exclude iteration results (regenerable run outputs) and
  the generated sales.csv fixture (deterministic, regenerate via
  Rscript generate-data.R)
The fixture apps are meant to be diagnosed blind. Remove comments that
flag the app as a deliberate exercise or name specific planted problems:
- '*before* fixture' header notes in both apps
- '(once per session is fine for now, right?)' next to the per-session read
- 'the market research API is slow' / 'slow external service' next to Sys.sleep
- 'so you can tell whether the app is responsive' on the live clock
The app directories are handed wholesale to the agent under eval, so the
READMEs listing every planted problem were effectively an answer key.
Move them to evals/GRADING.md (graders only) and reduce each app README
to regeneration instructions and a pointer to the grader notes.
Without context, a bare Sys.sleep() reads as deletable dead code, so an
eval agent could 'optimize' it away by simply deleting the line. Restore
the comments framing the sleeps as calls to a slow external service so
the delay is plausibly load-bearing.
From the eval-suite review:

- Rewrite fixture READMEs as in-universe docs; they no longer announce
  'fixture for evals' or point to evals/GRADING.md (which shipped inside
  the directory given to the agent under test)
- Delete the workspace-level work/ scratch tree, which contained a
  complete reference solution
- Add evals/RUNNING.md documenting the isolation protocol: run each eval
  from a clean git-archive export in a temp dir (no .git reachable),
  write eval_metadata.json only after runs complete, never run from the
  workspace root
- Drop the measurement-artifacts-saved assertions (eval-0/1/2); artifact
  hygiene is a skill concern, not a graded criterion
- Rescope req-gating-added to outcome-based outputs-gate-invalid-inputs;
  it false-failed two fully working, boot-verified apps
- De-steer all three eval prompts: remove the 'reactive graph' hint, the
  freeze/clock symptom detail, and the explicit 'profile it / top 3 /
  before-after numbers' asks; add a plausible red-herring user theory.
  eval-2's ranked-top3 assertion is reworded to prioritized diagnosis to
  match
Not part of the skill-creator schema for this file, unconsumed by the
aggregation/viewer scripts, and pointed at iteration directories that are
deliberately not committed.
SKILL.md:
- Recommend debrief over raw profvis: pv_* text summaries are made for
  agent consumption (overview, focus, hot lines, suggestions, compare)
- Reorganize the measurement block by who runs it: profvis/debrief and
  bench::mark are agent-runnable; reactlog is a user activity (agent can
  reconstruct the reactive graph from code) and shinyloadtest is an
  end-of-process check of a DEPLOYED app against target load
- Rework 'one change at a time': batch obvious, predictably-effective
  fixes and re-measure once; isolate changes only when attribution is
  genuinely uncertain
- Drop the Shiny for Python paragraph (separate skill planned)

async-tasks.md:
- Call out that ExtendedTask keeps BOTH the waiting session and other
  sessions responsive, unlike older promise strategies that only
  unblocked other users
- Point to the r-mirai skill (posit-dev/skills r-lib/r-mirai) for
  detailed mirai guidance

data-loading.md:
- Feature parquet alongside feather in the format table
- Name dbplyr as the lazy-SQL backend; add guidance on where to
  collect() (realize once into a shared reactive vs. stay lazy and
  collect at the end)
- Note DuckDB+httpfs can query parquet directly from S3
- Scope pool to real database servers; show a server example and note
  SQLite files / in-memory DuckDB need no pool

diagnosis.md:
- shinyapps.io is being sunset; cover Posit Connect Cloud (connect.posit.cloud)
  with the equivalent instance-sizing + run-choice knobs
- Fold debrief into the profvis reading guide; reframe reactlog as
  primarily a user-driven tool
Frees the shiny-optimize name for a future Shiny for Python skill.
Renames the skill directory, its eval workspace, and updates the
frontmatter name, marketplace.json registration, category README, and
eval suite references. Historical iteration records on disk keep the
old name deliberately.
The 1.1 bump from the review-notes commit is premature — the skill is
still unreleased in this PR.
Expand the Resources list for the shiny-r-optimize skill with the
sources used in its development: the async and bindCache articles,
Mastering Shiny, profvis/debrief, reactlog, shinyloadtest/shinycannon,
mirai/crew, DuckDB/pool, and Connect scheduler docs.
- async-tasks.md / SKILL.md / README.md / diagnosis.md: lead with mirai
  everywhere; ExtendedTask example rewritten with mirai + daemons();
  backends section swapped (mirai first); hard rule #1 feeds executed
  values into mirai() named args per schloerke's suggestion
- SKILL.md: explain the outputs/measurements/ folder layout (schloerke)
- data-loading.md: drop standalone vroom() row, note readr is vroom-powered
  (schloerke); add saveRDS()/readRDS() for non-tidy artifacts (cpsievert)
- reactive-graph.md: nuance debounce/throttle intro (Shiny's internal
  debouncing, bursts when compute outlasts the window), example is now
  event_data('plotly_brushing'), shinyreact note (cpsievert)
- rendering-ui.md: ExtendedTask quick reference (cpsievert)
- cpsievert's caching.md Connect persistent data dir comment: pending a
  reference link from Carson
reactive-graph.md: new subsection after the debounce/throttle guidance —
when smoothing isn't enough, gate the expensive update behind a trigger.
bslib::input_task_button() is a direct actionButton() drop-in: automatic
busy state from the flush alone, no server-side changes, no ExtendedTask.
submitButton() noted as the blunt legacy option. SKILL.md reactive ladder
and rendering-ui.md gate-speculative-outputs updated to match.
@gadenbuie
gadenbuie force-pushed the skill/shiny-optimize branch from f1bd14b to f7aed29 Compare October 14, 2026 20:26
@gadenbuie
gadenbuie merged commit ea89446 into main Oct 14, 2026
1 check passed
@gadenbuie
gadenbuie deleted the skill/shiny-optimize branch October 14, 2026 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants