# 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.
