Streamlit is one of the fastest ways to build a data app with Python. You write a script, run it, and you instantly have a working web app. But there's a catch — Streamlit re-runs your entire script from top to bottom every time a user clicks a button or changes a slider.
That's fine for small apps. But once you start loading large datasets, running ML models, or making API calls, this re-run behavior makes your app feel painfully slow. This guide covers the three main tools that fix this: caching, session state, and smart code structure.
How Streamlit Works (and Why It Gets Slow)
Before we fix the problem, let's understand it. Every time a user interacts with your app — clicking a button, typing in a text box, moving a slider — Streamlit re-runs your whole Python script from line 1 to the end.
In the example above, read_csv() runs every single time a user picks a new column. If the CSV is 100MB, your app freezes for a few seconds on every click. The fix is to tell Streamlit what to remember between runs.
Caching — The Biggest Speed Win
Caching means storing the result of a function so Streamlit can reuse it instead of running the function again. Streamlit gives you two decorators for this: st.cache_data and st.cache_resource.
st.cache_data — For Data
Use @st.cache_data for any function that loads or processes data — CSV files, API responses, database queries, computed results. Streamlit saves a copy of the output and returns it instantly next time.
You can also set a time limit so the cache refreshes automatically. This is useful for live data that changes every few minutes:
st.cache_resource — For Shared Objects
Use @st.cache_resource for things that should be shared across all users — like a loaded ML model, a database connection, or an API client. Unlike cache_data, this does not make a copy. Everyone uses the same object.
Quick Comparison: cache_data vs cache_resource
| Feature | st.cache_data | st.cache_resource |
|---|---|---|
| Best for | DataFrames, API results, computed values | ML models, DB connections, API clients |
| Makes a copy? | Yes — safe to mutate | No — shared object |
| Supports TTL? | Yes | No |
| Per-user or shared? | Per unique input | Shared across all users |
Session State — Remember Things Between Reruns
Caching stores the output of functions. But what about storing a user's choices, form inputs, or app state between reruns? That's what st.session_state is for.
Think of st.session_state as a dictionary that stays alive for the whole session. It doesn't reset when the script reruns — only when the user closes the browser tab.
Basic Usage
Common Patterns
A very common use case is multi-step forms or wizards — keeping the user on step 2 even after the script reruns:
Code Structure — Load Less, Render Faster
Even with caching and session state, a badly structured app will still feel slow. Here are the key structural rules that make a big difference.
Lazy Loading — Only Load What You Need
Don't load all your data at the top of the file. Load it only when the user actually needs it. This means putting data-loading calls inside if blocks or inside tabs.
st.fragment — Rerun Only Part of the Page
Added in Streamlit 1.33, @st.fragment is a game-changer. It lets you mark a function so that only that part of the page reruns when something inside it changes — the rest of the page stays still.
UI Speed Tips — Make It Feel Fast
Performance isn't just about raw speed — it's about perceived speed. These small UI tricks make your app feel much snappier even when it's doing heavy work.
- Streamlit reruns your whole script on every user interaction — this is the root cause of slowness.
- Use @st.cache_data for loading data, API calls, and computed results — they get stored and reused instantly.
- Use @st.cache_resource for shared objects like ML models and database connections — loaded once, used by everyone.
- Add TTL (ttl=300) to cache_data when your data updates regularly.
- Use st.session_state to remember user choices and computed results between reruns.
- Use @st.fragment to make only part of the page rerun when a widget changes — huge speed gain.
- Load data inside tabs or if blocks so it only runs when the user actually needs it.
- Use st.spinner(), st.progress(), and st.empty() to make slow operations feel fast.
