Streamlitアプリを劇的に高速化!st.fragmentによる部分実行の徹底解説

Streamlit
この記事は約15分で読めます。

こんにちは、JS2IIUです。

Pythonだけで驚くほど簡単にWebアプリを構築できるStreamlitは、データサイエンティストや機械学習エンジニアにとって、今やなくてはならないツールの一つです。アイデアを素早く形にし、分析結果やモデルのデモをインタラクティブに共有できる手軽さは、まさに革命的と言えるでしょう。

しかし、アプリケーションの機能が増え、複雑になるにつれて、ある共通の悩みに直面することがあります。それは、「ウィジェットを少し操作しただけで、アプリ全体が再読み込みされて動作がもっさりする」という問題です。スライダーを動かしたり、ドロップダウンリストから項目を選んだりするたびに、数秒間の待機時間が発生する…この小さなストレスが、ユーザー体験を大きく損ねてしまいます。

この現象の根本的な原因は、Streamlitのシンプルさの源泉でもある「上から下へスクリプト全体を再実行する」という実行モデルにあります。このモデルは状態管理を簡素化する一方で、UIの小さな変更が、本来再実行する必要のない重いデータ処理やAPIへの再アクセスまでトリガーしてしまうのです。

この記事では、このパフォーマンスの壁を乗り越えるための強力な武器、@st.fragmentデコレータを徹底的に解説します。@st.fragmentを使いこなすことで、アプリの特定の部分だけを独立して更新し、ユーザー体験を劇的に向上させる方法を、具体的なコードと共に学んでいきましょう。今回もよろしくお願いします。

Streamlitの実行モデルとパフォーマンスの壁

@st.fragmentの真価を理解するために、まずはStreamlitがどのように動作しているのか、その実行モデルを簡単におさらいしましょう。

Streamlitアプリは、ユーザーがブラウザ上で何らかのアクション(ボタンのクリック、テキスト入力など)を行うたびに、Pythonスクリプト全体が最初から最後まで再実行されます

このモデルは非常に直感的です。開発者は複雑なコールバック関数や状態管理を意識することなく、通常のPythonスクリプトを書く感覚でインタラクティブなアプリを構築できます。しかし、アプリが成長すると、このシンプルさが逆に足かせとなることがあります。

例えば、以下のような処理を含むダッシュボードアプリを考えてみてください。

  1. 重いCSVファイルやデータベースからデータを読み込む (pd.read_csv)
  2. 外部APIにリクエストを送り、最新の為替レートを取得する
  3. 読み込んだデータに対して、時間のかかる前処理や集計を行う
  4. 処理結果をインタラクティブなグラフで可視化する (st.altair_chart)
  5. グラフの表示期間を調整するためのスライダー (st.slider) を設置する

このアプリで、ユーザーがグラフの表示期間を少し変更するためにスライダーを動かしたとします。理想的には、グラフを描画する部分(ステップ4)だけが新しい期間で再実行されてほしいですよね。

しかし、Streamlitのデフォルトの挙動では、スライダーの操作をトリガーに、ステップ1から5までの全処理が再び実行されてしまいます。すでに読み込み済みのデータを再度読み込み、APIへも不要なリクエストを送り、同じ前処理を繰り返す…これこそが、「もっさり感」の正体です。この無駄な再実行が、パフォーマンスの大きなボトルネックとなるのです。

@st.fragmentとは?─ アプリに「部分実行」を指示する

このパフォーマンスの課題をエレガントに解決してくれるのが、@st.fragmentデコレータです。

@st.fragmentは、関数に付与することで、その関数を「独立した小さなStreamlitアプリ」のように扱えるようにする魔法のデコレータです。

どういうことかと言うと、@st.fragmentで修飾された関数(フラグメント関数)の内部にあるウィジェットが操作された場合、Streamlitはスクリプト全体を再実行するのではなく、そのフラグメント関数の中だけを再実行します

これは、家全体をリフォームするのではなく、キッチンの蛇口を交換したいときに、キッチンの中だけで作業を完結させるようなものです。他の部屋(アプリの他の部分)には一切影響を与えずに、目的の箇所だけを効率的に更新できます。

この「部分実行」の仕組みにより、インタラクティブなUIコンポーネントと、時間のかかるデータ処理ロジックを明確に分離し、アプリケーションの応答性を飛躍的に向上させることが可能になります。

【実践】@st.fragmentの基本的な使い方と比較

百聞は一見に如かず。@st.fragmentの効果を体感するために、具体的なコードでその違いを見ていきましょう。

Before: @st.fragmentを使わない従来のコード

まずは、意図的に重い処理を再現したシンプルなアプリです。このアプリには、実行に3秒かかる「重い処理」と、数値を入力するためのウィジェットがあります。

Python
import streamlit as st
import time

st.title("従来のStreamlitアプリ")

# 何か時間のかかる処理をシミュレート
@st.cache_data
def heavy_process():
    st.write("重い処理を実行中...")
    time.sleep(3)
    st.write("重い処理が完了しました。")
    return "処理結果"

# アプリのメイン部分
st.header("メインコンテンツ")
result = heavy_process()
st.write(f"メインコンテンツに「{result}」を表示しています。")

st.header("インタラクティブな入力エリア")
value = st.number_input("数値を入力してください", 0, 100, 0)
st.write(f"入力された数値: {value}")

このアプリを実行し、数値入力ボックスの値を変更してみてください。値を変更するたびに、コンソールと画面に「⏳ 重い処理を実行中…」というメッセージが表示され、3秒間の待機が発生することがわかります。これは、st.number_inputの操作によってスクリプト全体が再実行され、heavy_process()が毎回呼び出されているためです。(@st.cache_dataを使っているため2回目以降の実行は高速化されますが、ここでは初回実行やキャッシュがない状況を想定しています。)

After: @st.fragmentで高速化したコード

次に、同じアプリのインタラクティブな部分を@st.fragmentで分離してみましょう。

Python
import streamlit as st
import time

st.title("`@st.fragment`で高速化したアプリ")

# 時間のかかる処理は変更なし
@st.cache_data
def heavy_process():
    st.write("重い処理を実行中...")
    time.sleep(3)
    st.write("重い処理が完了しました。")
    return "処理結果"

# インタラクティブな部分を関数として切り出し、@st.fragmentで修飾
@st.fragment
def interactive_area():
    st.header("インタラクティブな入力エリア")
    value = st.number_input("数値を入力してください", 0, 100, 0)
    st.write(f"入力された数値: {value}")


# --- アプリの実行ロジック ---

# メインコンテンツは初回実行時のみ、またはキャッシュが切れた時のみ実行される
st.header("メインコンテンツ")
result = heavy_process()
st.write(f"メインコンテンツに「{result}」を表示しています。")

# 分離したインタラクティブなエリアを呼び出す
interactive_area()
このコードを実行して、同様に数値入力ボックスを操作してみてください。どうでしょうか?初回ロード時には「重い処理」が実行されますが、一度表示された後は、**数値を変更しても即座に表示が更新され、3秒間の待機が一切発生しない**はずです。

`interactive_area`関数内の`st.number_input`を操作しても、再実行されるのは`interactive_area`関数の中だけです。アプリのメイン部分にある`heavy_process()`は呼び出されなくなり、圧倒的に快適なユーザー体験が実現できました。これが`@st.fragment`の力です。

@st.fragmentを使いこなすための応用テクニック

@st.fragmentは単独でも強力ですが、他のStreamlit機能と組み合わせることで、さらにその真価を発揮します。

st.session_stateとの連携

フラグメントは独立して実行されますが、多くの場合、フラグメント内の操作結果をアプリの他の部分で利用したいでしょう。そのための最適な方法がst.session_stateです。st.session_stateは、再実行をまたいで変数の値を保持するための仕組みです。

Python
import streamlit as st

st.title("`st.fragment`と`session_state`の連携")

# session_stateの初期化
if "my_value" not in st.session_state:
    st.session_state.my_value = 0

# フラグメント内でsession_stateを更新する
@st.fragment
def settings_fragment():
    st.subheader("設定エリア")
    # session_stateの値をウィジェットのデフォルト値として使用
    current_value = st.slider(
        "値を設定してください",
        0, 100,
        st.session_state.my_value,
        key="slider_widget" # keyを指定して値をsession_stateに自動保存
    )
    # session_stateを直接更新することも可能
    st.session_state.my_value = current_value

# --- メインロジック ---

# フラグメントを呼び出す
settings_fragment()

# フラグメント外でsession_stateの値を参照する
st.subheader("メインエリア")
st.write(f"設定エリアで選択された値は **{st.session_state.my_value}** です。")
st.write(f"この値を使ってメインエリアで計算や表示ができます。")

# st.session_state.my_value を使った処理など...
if st.session_state.my_value > 50:
    st.success("値が50を超えました!")
else:
    st.info("値は50以下です。")

この例では、フラグメント内のスライダー(st.slider)の値をst.session_state.my_valueに保存しています。これにより、フラグメントの外側にあるメインエリアが、スライダーの最新の値をリアルタイムに参照して表示を更新できるようになります。

キャッシュ機能(@st.cache_dataなど)との違いと使い分け

パフォーマンス改善の文脈では、@st.cache_data@st.cache_resourceといったキャッシュ機能もお馴染みです。@st.fragmentとこれらのキャッシュ機能は目的が異なるため、適切に使い分けることが重要です。

機能トリガー(いつ実行されるか)主な用途コンセプト
@st.fragmentフラグメント内のウィジェット操作UIの応答性向上、インタラクティブな部分の分離実行範囲の限定
@st.cache_data関数の入力引数が前回から変更された場合重いデータロードや計算処理結果の再利用結果の再利用
@st.cache_resourceアプリのセッション中、初回のみDB接続やMLモデルのロードなど、一度だけ行いたい初期化リソースの共有

ベストプラクティスは、これらを組み合わせて使うことです。

  • 時間のかかるデータロードやAPIアクセス、重い計算処理は@st.cache_dataで関数を修飾し、結果をキャッシュする。
  • ユーザーが頻繁に操作するフィルター、スライダー、チャートなどのUIコンポーネント群は@st.fragmentで関数としてまとめ、部分実行の対象とする。

この2つを組み合わせることで、データ処理の無駄とUI更新の無駄の両方を削減し、アプリケーション全体のパフォーマンスを最大化できます。

実践的ユースケースから学ぶ@st.fragmentの活用シナリオ

最後に、@st.fragmentが特に輝く実践的なシナリオを2つ紹介します。

シナリオ1:インタラクティブなデータ分析ダッシュボード

大量の時系列データを分析するダッシュボードを考えてみましょう。データセット全体の読み込みと前処理は非常に重いため@st.cache_dataでキャッシュします。しかし、ユーザーが表示するデータの期間や移動平均のウィンドウサイズなどをスライダーで頻繁に変更する場合、そのたびにグラフの再描画だけでなく、関係のないKPI表示なども更新されてしまい、遅延が発生します。

このような場合、グラフと、その制御ウィジェット(スライダー、セレクトボックスなど)を一つの@st.fragment関数にまとめることで、フィルター操作に対するグラフの更新だけを高速に行うことができ、非常にスムーズな分析体験を提供できます。

シナリオ2:LLMチャットアプリケーション

ChatGPTのようなチャットアプリケーションでは、ユーザーがメッセージを送信するたびにUIが更新されます。このとき、過去のチャット履歴の表示、新しいメッセージの追加、そしてLLMからの応答のストリーミング表示といった処理が必要です。

もし@st.fragmentを使わない場合、メッセージを送信するたびに、過去の履歴も含めたページ全体が再描画され、ぎこちない動きになる可能性があります。ここで、チャットの入力ボックスと会話履歴の表示エリアを@st.fragmentで囲むことで、新しいメッセージのやり取りに関する部分だけを効率的に更新できます。これにより、まるでネイティブアプリのような、滑らかなチャット体験を実現できるでしょう。

まとめ

今回は、Streamlitアプリケーションのパフォーマンスとユーザー体験を劇的に向上させる@st.fragmentデコレータについて、その仕組みから実践的な使い方までを詳しく解説しました。

@st.fragmentは、Streamlitの「全体再実行」モデルの弱点を補い、アプリケーションの特定部分だけを独立して更新可能にする強力な機能です。

  • インタラクティブなUIの応答性を飛躍的に向上させる。
  • 重いデータ処理とUIロジックを明確に分離できる。
  • st.session_stateやキャッシュ機能と組み合わせることで、より高度で効率的なアプリを構築できる。

この機能は、Streamlitが単なるプロトタイピングツールから、より複雑で応答性の高い本番アプリケーションを構築するための本格的なフレームワークへと進化していることを示す、象徴的な一歩と言えるでしょう。

ぜひ、あなたのStreamlitアプリで最もインタラクティブな部分や、パフォーマンスのボトルネックとなっている箇所に@st.fragmentを導入してみてください。きっと、その効果に驚くはずです。

参考

最後まで読んでいただきありがとうございました。

コメント

タイトルとURLをコピーしました