# Make a slow Streamlit app fast: measure the rerun, then cache, fragment, or batch

Diagnose why a Streamlit app is slow on every interaction: time the suspect line, classify the bottleneck (recomputed results, full-rerun on local widget changes, per-keystroke reruns, heavy rendering), then fix with caching, st.fragment, st.form, or rendering reduction.

Exact reference: {"kind":"skill_version","skill_id":"skl_HM52DzYNvjmMSJDjPtNCXw","version_id":"skv_hfSveqO04vOyMP3jq4HMhQ"}

Applicability: [{"constraint":">=1.33 (st.fragment; experimental as st.experimental_fragment, stable name since 1.37)","technology":"Streamlit","version_scheme":"semver"}]

# Make a slow Streamlit app fast: measure the rerun, then cache, fragment, or batch

Use this when every click, slider move, or keystroke in a Streamlit app takes
seconds. The root cause is almost always the execution model: every widget
interaction reruns the entire script top to bottom, so any expensive line
re-executes on every interaction. Fix in order: measure, classify, then apply
the cheapest mechanism that fits.

## 1. Measure before changing anything

Time the suspect section so the fix is verifiable, not vibes:

```python
import time

t0 = time.perf_counter()
df = load_and_process()              # suspect line
st.caption(f"load_and_process: {time.perf_counter() - t0:.2f}s")
```

Interact with an unrelated widget. If the caption re-renders with a fresh
multi-second number, that line re-runs on every interaction and is your target.
Fix the target, then confirm the number drops to ~0s on subsequent interactions.

## 2. Classify the bottleneck

- **A. The same expensive result is recomputed on every rerun** (data load, CSV
  parse, model inference, DB query) → cache it. `@st.cache_data` for data
  (fresh copy per call), `@st.cache_resource` for one shared model/connection.
  Add `ttl` if the source changes over time.
- **B. A widget only affects one section, but heavy unrelated sections rerun
  with it** → isolate the interactive section with `st.fragment`. A fragment
  rerun re-executes only the fragment function; the rest of the app is frozen.
- **C. Users fill out many widgets and each keystroke triggers a rerun** →
  batch with `st.form` + `st.form_submit_button`; the app reruns once, on
  submit.
- **D. Rendering itself is slow** (huge `st.dataframe`, dozens of charts) →
  shrink what you render: aggregate or paginate before display, put secondary
  views behind tabs or expanders so they only render when opened, and render
  from cached data rather than recomputing per rerun.

## 3. Apply the fix

### Cache the recompute (A)

```python
@st.cache_data(ttl=3600)
def load_and_process():
    return expensive_pipeline()

df = load_and_process()   # runs once; later reruns reuse the cached value
```

### Fragment the interactive section (B)

`st.fragment` turns a function into an independently-rerunning section. When a
widget inside the fragment changes, only the fragment function re-executes;
everything else on the page is left untouched. (Available since Streamlit 1.33
as `st.experimental_fragment`; stable `st.fragment` name since 1.37.)

```python
import streamlit as st

def chart_section():
    metric = st.selectbox("Metric", ["revenue", "costs"])
    st.line_chart(df[metric])   # only this function reruns on change

chart_section = st.fragment(chart_section)
chart_section()                 # call it like any function
```

Rules that bite:

- A fragment rerun ignores the function's return value — share data with the
  rest of the app through `st.session_state`, not `return`.
- Widgets must live in the fragment's main body. Elements written to a
  container created *outside* the fragment are not cleared on fragment reruns;
  they accumulate duplicates until the next full-script rerun — guard with
  `st.empty` or write inside the fragment.
- Don't stack caching on a fragment function: `@st.cache_data` +
  `st.fragment` on the same function is unsupported.
- `st.rerun()` called inside a fragment triggers a *full* app rerun. To rerun
  just one fragment from a widget outside it, give the fragment a key and
  target it from a callback:

```python
def show_charts():
    st.write(f"Data: {st.session_state.get('filter', 'all')}")

show_charts = st.fragment(show_charts, key="charts")
show_charts()

st.selectbox(
    "Filter",
    ["all", "recent"],
    key="filter",
    on_change=lambda: st.rerun("charts"),   # fragment-only rerun
)
```

- `run_every="10s"` makes a fragment refresh itself on a timer — the cheap way
  to build a live dashboard section without rerunning the rest of the app:

```python
def live_status():
    st.line_chart(fetch_latest())

live_status = st.fragment(live_status, run_every="10s")
live_status()
```

### Batch inputs with a form (C)

```python
with st.form("filters"):
    region = st.selectbox("Region", ["us", "eu", "apac"])
    start = st.date_input("Start")
    submitted = st.form_submit_button("Apply")

if submitted:
    show_results(region, start)   # one rerun, after the user is done
```

Widgets inside a form don't trigger reruns until submit — and they can't
dynamically update each other in real time. That live interactivity is what
fragments are for; batching is what forms are for.

## 4. Verify and checklist

Interact with the widget that was slow. The timing caption from step 1 should
show ~0s for the cached or fragmented path, and unrelated sections should not
visibly recompute. If it's still slow, you classified wrong — re-measure; the
bottleneck is a different line.

1. Time the suspect section across two interactions.
2. Same result recomputed → cache it (`ttl` if the source changes).
3. One section's widgets rerun everything → `st.fragment` (return values
   ignored; share via session state).
4. Many inputs, one intent → `st.form` + submit button.
5. Rendering heavy → aggregate/paginate, defer behind tabs/expanders.
6. Re-measure; don't ship on vibes.


## Supporting basis and limitations

Built from the official Streamlit docs: the st.fragment API reference (signature, run_every, parallel, key, st.rerun("[key]") targeting) and the 'Working with fragments' concept page (fragment rerun semantics, return values ignored, outside-container accumulation, caching+fragments unsupported, fragments vs forms comparison).

## Change and rationale

New skill: diagnose and fix slow Streamlit apps via rerun profiling, caching, st.fragment, and st.form.

Every Streamlit interaction reruns the whole script, so slowness reports almost always reduce to an expensive line re-executing — but the fix depends on the bottleneck shape (cache vs fragment vs form vs render less). Forum advice jumps to one fix without measuring. This skill forces measurement first, then routes to the cheapest mechanism that fits.
