I love Streamlit for its speed of development. Being able to turn a Python script into a web app in minutes is a superpower. However, as my apps grew from simple prototypes to tools handling millions of rows of data, I hit a wall: the ‘Rerun’ problem. If you’ve felt your app stutter every time a user moves a slider, you’re dealing with the core challenge of streamlining streamlit performance optimization.
The fundamental issue is that Streamlit reruns the entire script from top to bottom whenever a user interacts with a widget. While this simplifies state management, it’s a nightmare for performance if you’re reloading a 500MB CSV or querying a slow API on every single interaction. In this deep dive, I’ll show you how to break this cycle and build apps that feel instantaneous.
The Challenge: The Execution Model Bottleneck
To optimize, we first have to understand the bottleneck. By default, Streamlit is stateless. Every interaction triggers a full script execution. When I first started building these tools, I didn’t realize that my pd.read_csv() call was happening every time a user changed a dropdown menu. This leads to high latency, wasted CPU cycles, and a frustrating user experience.
If you’re still deciding between frameworks, you might want to check out my comparison of Streamlit vs Dash for data apps to see which execution model fits your needs better. But if you’re committed to Streamlit, the solution lies in selective execution.
Solution Overview: The Optimization Pyramid
I categorize performance wins into three layers: Caching (avoiding redundant work), Session State (preserving data), and Fragments (reducing the rerun scope). By applying these in order, you move from an app that feels like a script to one that feels like a professional software product.
Techniques for High-Performance Streamlit Apps
1. Mastering @st.cache_data and @st.cache_resource
Caching is the single most impactful way of streamlining streamlit performance optimization. Streamlit provides two distinct decorators: @st.cache_data for data (DataFrames, lists) and @st.cache_resource for global objects (Database connections, ML models).
import streamlit as st
import pandas as pd
import time
@st.cache_data
def load_massive_dataset(file_path):
# This only runs once unless the file_path changes
time.sleep(3) # Simulating a slow network load
return pd.read_csv(file_path)
df = load_massive_dataset("large_data.csv")
st.write(df)
In my experience, the biggest mistake developers make is caching functions that depend on frequently changing variables. Keep your cached functions pure—they should only depend on the arguments you pass to them.
2. Reducing Rerun Scope with @st.fragment
Introduced in recent versions, fragments allow you to rerun only a specific function instead of the whole page. This is a game-changer for apps with a “global” header and a “local” interactive chart.
@st.fragment
def interactive_chart():
val = st.slider("Adjust threshold", 0, 100, 50)
# Only this function reruns when the slider moves!
st.line_chart(data[data['value'] > val])
st.title("My Heavy Dashboard")
# This part doesn't rerun when the slider in the fragment moves
st.write("Global Header Content")
interactive_chart()
3. Optimizing Data Visualization
The way you render data is just as important as how you load it. Using heavy libraries like Plotly for simple charts can slow down the browser. I often recommend looking at the best python data visualization library 2026 list to find a balance between interactivity and render speed.
As shown in the benchmark chart below, switching from full-page reruns to fragmented updates can reduce perceived latency by up to 80%.
Implementation Strategy: A Case Study
I recently optimized a financial reporting tool that took 12 seconds to update. Here was my workflow:
- Step 1: Wrapped the SQL query in
@st.cache_datawith a 1-hourttl. (Load time: 12s $\rightarrow$ 0.1s on rerun). - Step 2: Moved the filtering logic into a
@st.fragment. (UI lag: 2s $\rightarrow$ 0.3s). - Step 3: Replaced a heavy Plotly 3D scatter plot with a streamlined Altair chart. (Browser render: 1.5s $\rightarrow$ 0.4s).
Pitfalls to Avoid
- Over-caching: Caching everything can lead to memory leaks and “stale data’ bugs. Use the
ttlparameter to expire old data. - Large DataFrames in Session State: Storing massive datasets in
st.session_statecan bloat the server’s RAM. Keep raw data in the cache and only store filtered indices or metadata in the state. - Ignoring the Browser: Remember that Streamlit is a Python wrapper. If you send a 100MB DataFrame to
st.dataframe(), the browser will crash regardless of how fast your Python code is. Usedf.sample()or pagination.
Ready to scale your data tools? If you’re building for an enterprise environment, make sure you’re following a consistent architecture to avoid technical debt.