shiny-py-optimize について
既存のShiny for Pythonアプリを動作を変えずに診断し、高速化するスキルです。共有asyncioイベントループを考慮し、処理の削減や再利用を優先してから、ブロッキング処理の移動を検討します。
主な機能
- 01リアクティブ処理と描画のボトルネック調査
- 02リアクティブ計算による重複処理の削減
- 03キャッシュとイベントによる入力制御
- 04データ読み込みとテーブル描画の改善
- 05動作を保ちながら変更を測定
活用シーン
- 01セッションごとのデータ読み込み遅延を減らす
- 02入力変更後の再計算を抑える
- 03長時間処理による全セッションの停止を防ぐ
- 04コード変更前後の性能を比較
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ posit-dev/ skills --skill shiny-py-optimizeインストール後、Claude Code で/shiny-py-optimizeを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/async-tasks.mdcaching.mddata-loading.mddiagnosis.mdreactive-graph.mdrendering-ui.md詳細
Shiny for Python アプリのパフォーマンス最適化
既存の Shiny for Python アプリの機能を変えずに、高速化します。 一般的なケースでは、1 人または 2 人の開発者で問題なく動作していたアプリが、遅くなりすぎたか、より多くのユーザーをサポートする必要が生じています。アプリのロジックはすべてそのままにし、デプロイ設定の調整よりもコードの変更を優先し、書き直しは避けてください。
各修正の優先順位は次のとおりです。
- 不要な処理をなくす(誰も見ていない出力を計算しない、入力の準備が整っていない間は計算しない)。
- 繰り返される処理を再利用する(共有の
@reactive.calc、モジュールレベルのキャッシュ)。 - 避けられない処理を高速化する(より高速なデータ形式、polars、プッシュダウン)。
- ブロックする処理を別の場所に移す(
@reactive.extended_task)— 最終手段です。これは並行性を高めるものであり、処理そのものを速くするものではありません。
Python 固有のアドバイスの大半を左右する事実が 1 つあります。**Shiny はリアクティブ関数を単一の asyncio イベントループ上で逐次実行します。async def であっても、同じプロセス内のセッション間であっても、同時実行されることはありません。**リアクティブ関数内のブロッキング呼び出しが 1 つあるだけで、そのプロセス内のすべてのセッションが停止します。async/await だけでは解決しません。解決できるのは @reactive.extended_task だけです。
ワークフロー
1. 問題を把握する
「遅い」とは何を意味するのかを明確にし、改善目標となるベンチマークとして記述します。
- 何が遅く感じられますか? 最初のページ読み込みですか?操作のたびですか?特定の操作だけですか?複数の人が同時に使うときだけですか?
- 同時に何人のユーザーをサポートする必要がありますか?(1~5 人、約 20 人、数百人など)
- どこで実行されていますか? ローカル、Posit Connect、Posit Connect Cloud、セルフホスト(nginx の背後にある uvicorn)ですか?
- データの規模はどれくらいで、どこに保存され、どのくらいの頻度で変更されますか?
- Core と Express のどちらですか?(
from shiny import ...とfrom shiny.express import ...の違いです)。Express のトップレベルコードはセッションごとに 1 回実行されます。これは気づきにくい典型的な落とし穴です(data-loading.md)。
2. コードの問題点をスキャンして、その場で修正します
アプリのコード(app.py、モジュール)を読み、よくある問題点を探します。この表にある問題点はすべて、動作を変えずに安全に修正できます。見つけた問題は、測定を始める前にすぐ修正してください。これらの変更で、動作が遅い Shiny for Python アプリの大部分を改善できます。
| 問題点(検索対象) | 問題となる理由 | 修正方法 |
|---|---|---|
server() や render 関数内の pd.read_csv/DB クエリ | セッションごと、または無効化のたびに再実行されます | プロセスごとに一度だけ読み込みます。Core ではモジュールスコープ、Express ではインポートしたモジュールを使います → data-loading.md |
Express の app.py のトップレベルでのデータ読み込み | トップレベルの Express コードはセッションごとに一度実行されます | 読み込みをインポートしたモジュール(shared.py)に移します → data-loading.md |
| 複数の出力で同じサブセット化/集計を繰り返す | 変更のたびに重複した計算が発生します | 共有の @reactive.calc にまとめます → reactive-graph.md |
req() ガードのない、コストの高い @render.* | 起動時に空の入力や準備途中の入力に対して計算します | 本体の先頭に req() を追加します → reactive-graph.md |
| ランディングビューに重い出力が多数ある | ページを使える状態にする前に、すべての計算が必要になります | タブ/ナビゲーションに移します。非表示の出力は実行が中断されます → rendering-ui.md |
| 同一のコストの高い処理を複数のセッションで繰り返す | ユーザーごとに同じコストが発生します | モジュールレベルのキャッシュ(functools.lru_cache、diskcache)を使います。組み込みの bindCache 相当機能はありません → caching.md |
| スライダー/テキスト入力がコストの高い処理の連鎖に直接つながっている | ドラッグするたびに何十回も再計算されます | @reactive.event() でボタンによる実行制御を行います。デバウンス用ヘルパーも使えます → reactive-graph.md |
計算/エフェクト/レンダー内での time.sleep、同期 requests、負荷の高い pandas/DuckDB 呼び出し | プロセス内のすべてのユーザーのイベントループをブロックします | @reactive.extended_task を使います → async-tasks.md |
高速化を期待して追加した async def のリアクティブ | Shiny は async であってもリアクティブコードを逐次実行します | async 専用 API には async を使い、ノンブロッキング処理には ExtendedTask を使います → async-tasks.md |
通常の値更新に @render.ui を使用している | 更新のたびに DOM が再構築され、入力が再バインドされます | ui.update_*() を使います → rendering-ui.md |
大きなデータフレームに @render.table を使用している | pandas Styler が HTML 全体をシリアライズします | render.DataGrid を使う @render.data_frame にします → rendering-ui.md |
次のおおまかな順序で修正を適用します(手間の少ないものから):データをモジュールスコープ(または Express ではインポートしたモジュール)に読み込む → req() ガード → 共有の @reactive.calc → 重い出力をタブに移す → モジュールレベルのキャッシュ → 入力頻度の高い処理を @reactive.event() で制御 → @render.ui の代わりに ui.update_*() を使う → 高速なデータ形式(polars、parquet、DuckDB)を使う → 数秒かかる処理に @reactive.extended_task を使う → 残る待ち時間にビジーインジケーター(ui.busy_indicators)を表示する。
3. 測定する — 判定ではなく、確認のために
測定は修正を確認し、スキャンでは見つからない問題を探すために行います。測定に追われて、修正がおろそかになってはいけません。 アプリの実行が難しい場合、トレースが遅い場合、または結果が曖昧な場合でも、スキャンで見つかった問題は修正し、確認できなかった点を明記してください。
Shiny(≥ 1.6)には、リアクティブ処理をトレースする OpenTelemetry 機能が組み込まれています。これは、どのコードが遅いかを調べるための主要なツールです。
uv pip install "shiny[otel]"
SHINY_OTEL_COLLECT=reactivity \
opentelemetry-instrument --traces_exporter console --logs_exporter console \
--metrics_exporter none shiny run app.py
アプリを操作してから、スパンの階層を確認します。遅い reactive.calc/出力/拡張タスクはすべて、ソースファイルと行番号とともに表示されます。CPU フレームグラフが必要な場合は、実行中のプロセスを py-spy でサンプリングします。2 つの実装を比較するには timeit を使います。「このアプリは何人のユーザーに対応できるか」を調べるには、デプロイしたアプリに対して shinyloadtest で負荷テストを実行します(記録 → N 個のワーカーで再生 → レポート)。ツールの選択方法と読み方のガイドについては、diagnosis.md を参照してください。
4. スキャンで見つからなかった問題を診断する
| 症状 | 最も可能性の高い原因 | まず確認する項目 |
|---|---|---|
| 初回ページの読み込みが遅い(毎回のセッション) | セッションごとのデータ読み込み(特にExpressのトップレベル);空の入力で起動時に出力を計算している;セッションごとの重いセットアップ | data-loading.md、reactive-graph.md |
| 1つの操作で過剰な再計算が発生する | 依存関係の範囲が広すぎる;計算の重複;頻繁に更新される入力 | reactive-graph.md、caching.md |
| 特定の操作に数秒かかり、アプリがフリーズする — 全ユーザーに影響する | 共有イベントループ上のブロッキング処理 | async-tasks.md |
| 1人のユーザーでは高速だが、複数人になると遅くなる | ブロッキング処理;同一計算の繰り返し;ユーザーごとの再読み込み;プロセス数が少なすぎる | caching.md、async-tasks.md、diagnosis.md |
| プロット/テーブルの表示や更新が遅い | 重いレンダリング;@render.table;全体の再描画;集約されていないPlotly | rendering-ui.md |
| ダウンロード/アップロードの動作に問題がある、または再計算される | クリックごとに再計算するダウンロードハンドラー;レンダラー内で解析されるアップロード | data-loading.md |
| デプロイ後だけ遅くなる | プロセスごとのグローバル変数とキャッシュ;プロセス/接続の上限 | diagnosis.md |
5. まずは変更コストの低い修正から
診断したカテゴリの修正手順を進め、計測結果から十分に高速だと判断できた時点で止めます。各段階でアプリの動作は維持されます。
リアクティブな再計算 → reactive-graph.md
req()ガード — 入力の準備ができていなければ何も計算されません。- 共有の
@reactive.calc— 変更ごとに各派生値を一度だけ計算します。 - 依存関係の範囲を絞る:
@reactive.event()、reactive.isolate()、更新ループにはreactive.value.freeze()を使用します。 - 頻繁に更新される入力をデバウンスします(ヘルパーを使用 — Shiny 1.8時点では組み込み機能ではありません)。更新が連続する場合は、
ui.input_task_button()(ui.input_action_button()と差し替えて使用できます)で一括処理を制御します。 - タイマーを修正します:
invalidate_later()は最後にスケジュールし、@reactive.poll()には低コストのチェック関数を使います。
同一処理の繰り返し → caching.md
@reactive.calcはセッションごとに最新の値をメモ化します — 追加コストはかかりません。- モジュールレベルの純粋関数(DBクエリ、モデルのフィッティング)には
@functools.lru_cacheを使用します — キーには結果に影響するすべての入力を含める必要があります。 - プロセス間で結果を共有する必要がある場合、再起動後も結果を保持する必要がある場合、またはデータが時間とともに変化する場合は、
diskcache/cachetools.TTLCacheを使用します。 - キャッシュ結果は読み取り専用の共有状態として扱います — 変更する前にコピーを返します。
ブロッキング処理 → async-tasks.md
- 処理の除去とキャッシュを試し尽くした後にのみ、
ui.input_task_buttonと@ui.bind_task_buttonを併用して@reactive.extended_taskを使用します。 - タスク内の同期I/Oには
asyncio.to_thread(...)を使用します。CPU負荷の高い処理にはモジュールレベルのProcessPoolExecutorを使用します(通常のスレッドではCPU処理は高速化されません)。 - タスク本体内でリアクティブ値を読み取らず、
.invoke()を介して値を渡します。
データアクセス → data-loading.md
- データはプロセススコープ(Coreではモジュールスコープ、Expressではインポートしたモジュール)に読み込みます。レンダー関数内でデータを読み込まないでください。
- サイズの大きいデータにはpandasよりpolarsを、CSVよりparquetを使用します。
- データがRAMの容量を超える場合や一部のデータだけが必要な場合は、parquetにはDuckDBを使用するか、遅延評価の
pl.scan_parquet(...).filter(...).collect()を使用します。
レンダリングとUI → rendering-ui.md
- 重い出力はタブ/ナビゲーション内に配置し、非表示の出力を中断状態に保ちます。
- ボタン(
req()+@reactive.event())で投機的な出力を制御します。 - テーブルには
@render.data_frame+update_data()を使用し、大きなPlotlyにはWebGL/集約を使用します。処理中はビジーインジケーターを表示します。
6. 検証と反復
変更するたびに、1つずつ再測定し、ベースラインの数値と比較します。観測可能な動作は同一に保ってください。機能が削られた「高速な」アプリは、高速とはいえません。完了する前に、代表的な出力1つの値を元のアプリと変更後のアプリ(または生データ)から再計算し、一致することを確認してください。これにより、気づかないうちに動作が変わっていないかを低コストで検出できます。
レポートの整合性:引用する測定結果(トレース出力、タイミングログ)はすべて、アプリのプロジェクト内にある outputs/measurements/ に保存します。フォルダーが存在しない場合は作成し、最終レポートで主張する各数値と仕組みを、保存した成果物に照らして確認してください。成果物で検証できない主張は、断言するのではなく、レポートにその旨を記載してください。
<app-project>/
outputs/
measurements/ # the audit trail for the final report: one file per
# measurement (trace output, py-spy profile, timings),
# named for the fix or code path it measured
基本ルール
- 明らかなものはまとめ、不確かなものは切り分けます。 予測可能な効果が見込める安全な修正、たとえばデータ読み込みをモジュールスコープに移す、重複したフィルタリングを統合する、
@render.uiを置き換えるといった変更は、まとめて適用して一度だけ再測定できます。どの変更が効果をもたらしたか本当に不明な場合や、変更が期待した効果をもたらさなかった場合に限り、1つずつ変更してください。そのような場合にだけ、変更の効果を特定するための追加測定に価値があります。 - 動作を維持してください。 UI の変更は、体感性能を高めるもの(スピナー、タスクボタン、タブ)に限定し、ユーザーに明示してください。また、出力が同一でも、タイミングの意味を変える変更(デバウンスは意図的な遅延を加え、キャッシュはセッション間で結果を共有します)も同様に明示してください。
asyncはパフォーマンス改善策ではありません。async defのリアクティブは、非同期専用 API を呼び出すために存在します。並行実行はせず、アプリの応答性も向上させません。ノンブロッキングにするには@reactive.extended_taskを使います。- 非同期処理より先にキャッシュを検討してください。 キャッシュで 10~100 倍高速化することもよくあります。非同期処理がもたらすのは並行性だけで、シリアライズやデバッグのコストも伴います。
- マルチワーカーサーバーでスケールさせないでください。
uvicorn --workers >1、WEB_CONCURRENCY環境変数、マルチワーカーの Gunicorn は、Shiny をひそかに破損させます(スティッキーセッションが必要です。diagnosis.md を参照)。スティッキーセッション対応のロードバランシングの背後で、シングルワーカープロセスを複数使用してスケールさせてください。 - 依存関係(polars、duckdb、diskcache、py-spy など)を追加する前に確認してください。
参考資料
必要なときに、診断結果が示す参考資料だけを読んでください。最初からすべてに目を通す必要はありません。
- diagnosis.md — OpenTelemetry トレーシング、py-spy、タイミングの確認、shinyloadtest による負荷テスト、シングルプロセスの asyncio モデル、スティッキーセッション、デプロイ容量の調整項目。
- reactive-graph.md — 共有計算、
req()、reactive.isolate()、@reactive.event()、更新ループとreactive.value.freeze()、デバウンスヘルパー、タイマー、ポーリング。 - caching.md —
@reactive.calcがキャッシュするものとしないもの、モジュールレベルのlru_cache/diskcache/cachetoolsのパターンと厳守事項。 - async-tasks.md — ブロッキングがすべてのユーザーに悪影響を及ぼす理由、
@reactive.extended_taskのパターンと厳守事項、スレッドとプロセスプール。 - data-loading.md — プロセススコープでの読み込み(Core と Express)、polars/parquet/DuckDB の遅延読み込み、データベース、ダウンロード、アップロード。
- rendering-ui.md — タブによる出力の一時停止、
@render.uiの代替手段、プロット/テーブルの描画コスト、体感性能。