Streamlit's st.fragment: Partial Reruns for Faster Dashboards
Streamlit's st.fragment decorator isolates UI sections so only that fragment re-runs on widget changes — cutting dashboard latency from seconds to milliseconds without replacing caching.
31 Dec 2025, 19:09 UTC

The Full-Rerun Bottleneck
Streamlit's execution model is simple: every widget interaction re-runs the entire script from top to bottom. This works well for small apps, but on data-heavy dashboards — think a 50 MB Parquet load, a few aggregations, and three Plotly charts — a single dropdown change can freeze the UI for seconds. Caching with st.cache_data helps, but the script still re-executes every line, re-entering cached functions, re-rendering static markdown, and re-sending unchanged DOM to the browser.
Takeaway: st.fragment (stabilized in Streamlit 1.35, July 2024) lets you isolate a function's UI so only that fragment re-runs on its internal widget changes. The rest of the script stays static. On a real dashboard this cuts perceived latency from seconds to milliseconds.
How Fragments Differ from Caching
st.cache_data and st.cache_resource memoize computation — they return a stored result without re-running the function body. st.fragment does not memoize anything. It limits UI re-rendering: the fragment function still executes on every relevant widget change, but the main script does not. You still need cached functions for expensive data loads; fragments just prevent the surrounding boilerplate from re-running.
Under the hood, Streamlit assigns each fragment a unique ID and a separate message channel. When a widget inside a fragment changes, the browser sends only that fragment's widget values. The server executes just the fragment function and returns a delta for that fragment's DOM region. The main script's widgets and layout are untouched.
Worked Example: Dashboard with Local Filter
Consider a sales dashboard. The main script loads a 50 MB dataset once (cached), renders a top-level region selector, and then delegates the detailed chart + its own product filter to a fragment.
import streamlit as st
import pandas as pd
import plotly.express as px
@st.cache_data
def load_sales() -> pd.DataFrame:
# Simulate expensive load
return pd.read_parquet("s3://bucket/sales.parquet")
df = load_sales()
st.title("Sales Dashboard")
region = st.selectbox("Region", df["region"].unique())
filtered = df[df["region"] == region]
@st.fragment
def chart_fragment(data: pd.DataFrame):
product = st.selectbox("Product", data["product"].unique(), key="product_frag")
subset = data[data["product"] == product]
fig = px.line(subset, x="date", y="revenue")
st.plotly_chart(fig, use_container_width=True)
chart_fragment(filtered)
What happens:
- On first load,
load_salesruns, the title and region selector render, thenchart_fragmentruns and renders its product selector and chart. - Changing the Region selector (top-level) triggers a full rerun:
load_salesreturns cached data,filteredrecomputes, and the fragment re-renders with the new dataset. - Changing the Product selector (inside the fragment) triggers only
chart_fragment. The main script does not re-execute.load_salesis not re-entered. The region selector keeps its value.
You can verify this by adding print("main run") at the top of the script and print("fragment run") inside chart_fragment. Open the server logs: the main print appears once on load and again only when Region changes. The fragment print appears on every Product change.
Constraints You'll Hit
- No nesting. A fragment cannot contain another
@st.fragment. Streamlit raisesStreamlitAPIExceptionat runtime. - No layout containers. Fragments don't work inside
st.columns,st.tabs,st.expander, or other containers that manage their own layout. Place the fragment at the top level or inside a plainst.container(). - State ownership. A fragment can read
st.session_statebut should not write keys that other fragments or the main script depend on. Writes are visible to the main script only on the next full rerun, not to sibling fragments in the same run — this creates stale reads and infinite-loop risks. - No
st.rerun(). The fragment auto-reruns on its own widget changes. Callingst.rerun()inside a fragment is an error. run_everypolling. Therun_every="5s"parameter triggers a full fragment rerun on a timer. If the fragment takes longer than the interval, updates stack up. Prefer user-driven updates; use polling only for live indicators where slight drift is acceptable.
Migration from Experimental
If you used @st.experimental_fragment (introduced in 1.32), migration is drop-in: change the import to from streamlit import fragment or use @st.fragment. The signature is identical — run_every is still supported. No other code changes required.
Quick Verification Checklist
- Create a minimal app with a slow cached load, a top-level widget, and a fragment wrapping a chart + local widget.
- Add print statements in the main script and inside the fragment. Confirm main prints once on load; fragment prints only on its widget changes.
- Set
run_every="5s"on a fragment that displayspd.Timestamp.now(). Verify it updates every 5 seconds without interaction. - Attempt to nest a fragment inside another fragment or inside
st.tabs— confirm the runtime exception.
If all four checks pass, you've confirmed the fragment boundary behaves as documented for your Streamlit version (1.35+).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.