Mục lục
Mục tiêu bài học
Sau bài này bạn sẽ:
- Hiểu rõ sự khác biệt execution model giữa Gradio và Streamlit và tại sao nó ảnh hưởng đến cách thiết kế app.
- Biết framework nào phù hợp với use case nào dựa trên tiêu chí kỹ thuật cụ thể.
- Tránh chọn sai framework rồi mới phát hiện giới hạn ở giữa chừng dự án.
- Nhận ra khi nào cả hai đều không phù hợp.
Bài này không dạy lại syntax của hai framework — xem bài 9 (Gradio) và bài 10 (Streamlit) nếu cần ôn lại. Bài này tập trung hoàn toàn vào so sánh kỹ thuật và quyết định chọn lựa.
Execution model — khác biệt cốt lõi
Đây là điểm khác biệt nền tảng nhất — ảnh hưởng đến mọi thứ từ cách viết code đến performance.
Gradio: event-driven
Gradio chạy theo mô hình event-driven. Mỗi component đăng ký handler riêng. Khi user click button, chỉ callback tương ứng được gọi — phần còn lại của app không chạy lại.
import gradio as gr
def process(text: str, threshold: float) -> str:
return f"Processed: {text} (threshold={threshold})"
with gr.Blocks() as demo:
inp = gr.Textbox(label="Input")
slider = gr.Slider(0.0, 1.0, value=0.5)
btn = gr.Button("Run")
out = gr.Textbox(label="Output")
# Chỉ hàm process() chạy khi click btn
btn.click(fn=process, inputs=[inp, slider], outputs=out)
demo.launch()
Streamlit: script rerun
Streamlit chạy lại toàn bộ file Python từ đầu đến cuối mỗi lần user tương tác. Click button, kéo slider, gõ text input — đều trigger toàn bộ script chạy lại.
import streamlit as st
# Script này chạy lại từ đầu mỗi lần user tương tác
text = st.text_input("Input")
threshold = st.slider("Threshold", 0.0, 1.0, 0.5)
if st.button("Run"):
# Chỉ đoạn code này phụ thuộc vào button
# nhưng toàn bộ script đã chạy đến đây rồi
st.write(f"Processed: {text} (threshold={threshold})")
Điều gì xảy ra khi user click button
| Hành vi | Gradio | Streamlit |
|---|---|---|
| Code Python được chạy | Chỉ callback đã đăng ký (btn.click) |
Toàn bộ script từ dòng 1 đến EOF |
| Biến Python thông thường | Giữ nguyên trạng thái giữa các call | Reset về giá trị ban đầu mỗi rerun |
| Model load bên ngoài cache | Không bị load lại | Load lại mỗi rerun nếu không cache |
| Phù hợp khi script dài | Tốt — code ngoài callback không bị gọi | Cần cẩn thận — mỗi dòng đều chạy lại |
Hệ quả thực tế: Streamlit yêu cầu lập trình viên phải thiết kế cho việc chạy lại — cache, session_state, st.form để giảm rerun không cần thiết. Gradio không có vấn đề này vì chỉ chạy handler được gọi.
Layout và UX
Gradio
gr.Interface tạo layout cố định: input bên trái, output bên phải, nút Submit ở giữa. Phù hợp cho kiểu app "nhập vào → nhận kết quả". Layout này quen thuộc với người dùng cuối không có kỹ thuật.
gr.Blocks cho phép tự khai báo layout với gr.Row, gr.Column, gr.Tab. Tuy nhiên thiết kế vẫn nghiêng về dạng form: input → trigger → output. Khó xây dashboard nhiều tầng từ top đến bottom vì cấu trúc layout của Gradio không tự nhiên với scroll dài.
Theme: Gradio hỗ trợ gr.themes.Base(), gr.themes.Soft(), gr.themes.Monochrome(). Có thể custom qua theme builder tại gradio.app/guides/theming-guide.
with gr.Blocks(theme=gr.themes.Soft()) as demo:
...
Streamlit
Streamlit dùng mô hình vertical scroll — component được render từ trên xuống dưới theo thứ tự trong code. st.columns() tạo multi-column, st.sidebar tạo panel trái cố định, st.tabs() tạo tab ngang. Phù hợp với dashboard layout: sidebar filter + nội dung chính với nhiều chart.
Theme: cấu hình qua file .streamlit/config.toml:
[theme]
primaryColor = "#1f77b4"
backgroundColor = "#ffffff"
secondaryBackgroundColor = "#f0f2f6"
textColor = "#262730"
font = "sans serif"
Kết luận layout
Gradio phù hợp khi UI là form input/output đơn giản. Streamlit phù hợp khi cần dashboard phức tạp với filter sidebar, nhiều chart và bố cục nhiều cột.
Tốc độ build nguyên mẫu
Demo 1 model classifier
Gradio — 5 dòng:
import gradio as gr
def classify(text: str) -> dict:
# giả sử model đã load ở module level
return {"positive": 0.82, "negative": 0.18}
gr.Interface(fn=classify, inputs="text", outputs=gr.Label()).launch()
Streamlit — 10–15 dòng:
import streamlit as st
st.title("Text Classifier")
text = st.text_area("Input text")
if st.button("Classify"):
# giả sử model đã load với @st.cache_resource
result = {"positive": 0.82, "negative": 0.18}
for label, score in result.items():
st.metric(label=label, value=f"{score:.0%}")
Với use case demo 1 model, Gradio ít boilerplate hơn rõ ràng. gr.Interface tự tạo label, layout, nút Submit và hiển thị output.
Dashboard 5 metric + filter + chart
Streamlit — ~30 dòng: sidebar filter, 3 metric card, 1 line chart, 1 dataframe — có thể xây gọn trong 30 dòng vì component model của Streamlit thiên về top-down composition.
Gradio — ~60–80 dòng: cần gr.Blocks + nhiều gr.Row/gr.Column + quản lý event handler riêng cho mỗi filter. Sidebar là khái niệm không có trong Gradio — phải tự tạo bằng column layout.
Tốc độ runtime và chi phí
Gradio — nhanh hơn khi click đơn
Vì Gradio chỉ gọi callback đã đăng ký, mỗi lần click chỉ tốn thời gian của hàm đó. Với app đơn giản (1 button, 1 callback), latency phía Python rất thấp — phần lớn thời gian là inference model.
Streamlit — ảnh hưởng bởi độ dài script
Mỗi rerun chạy toàn bộ script. Nếu script có nhiều bước khởi tạo nặng không được cache, mỗi click đều chậm. Ví dụ: nếu quên @st.cache_resource trên model loader, mỗi rerun load lại model 500MB.
Với cache đúng cách (@st.cache_data, @st.cache_resource), Streamlit runtime tương đương Gradio cho các tác vụ thông thường.
Vấn đề với DataFrame lớn trong Streamlit
Streamlit serialize DataFrame sang JSON để gửi về browser. 100k dòng thường mất vài giây và render chậm. Nên luôn giới hạn số dòng hiển thị:
# Tránh st.table() với data lớn — render cả bảng tĩnh
# Dùng st.dataframe() với limit
st.dataframe(df.head(500))
st.caption(f"Hiển thị 500 / {len(df):,} dòng")
Gradio cũng có component gr.Dataframe nhưng cũng gặp vấn đề tương tự với data lớn. Cả hai đều không phải công cụ phù hợp để hiển thị dataset nhiều triệu dòng.
State management
Gradio: gr.State() per session
gr.State(default_value) tạo một ô nhớ per-session. Giá trị được truyền vào function như argument bình thường và trả về để cập nhật.
import gradio as gr
def add_to_history(user_msg: str, history: list) -> tuple[list, str]:
history = history + [user_msg] # không mutate list gốc
return history, f"Đã thêm: {user_msg}. Tổng: {len(history)}"
with gr.Blocks() as demo:
state = gr.State([]) # list rỗng ban đầu, per session
msg_input = gr.Textbox(label="Message")
btn = gr.Button("Add")
summary = gr.Textbox(label="Summary")
btn.click(
fn=add_to_history,
inputs=[msg_input, state],
outputs=[state, summary], # state được cập nhật, không hiển thị ra UI
)
demo.launch()
Pattern này rõ ràng: state đi vào function, state mới đi ra function. Không có side effect ngầm.
Streamlit: st.session_state dict
st.session_state là dict tồn tại xuyên suốt các rerun trong cùng session. Truy cập trực tiếp từ bất kỳ đâu trong script.
import streamlit as st
if "history" not in st.session_state:
st.session_state.history = []
msg = st.text_input("Message")
if st.button("Add"):
st.session_state.history.append(msg)
st.write(f"Tổng: {len(st.session_state.history)}")
Tiện lợi hơn vì truy cập trực tiếp, nhưng dễ có side effect nếu script phức tạp — state bị đọc và ghi từ nhiều nơi. Gradio buộc state đi qua function signature, rõ ràng hơn về data flow.
So sánh nhanh
| Tiêu chí | Gradio (gr.State) | Streamlit (session_state) |
|---|---|---|
| Cú pháp truy cập | Qua function args/return | Dict toàn cục st.session_state["key"] |
| Data flow | Tường minh qua signature | Ngầm — đọc/ghi từ bất kỳ đâu |
| Dễ debug | Hơn khi nhiều handler | Cần trace flow thủ công khi phức tạp |
| Tích hợp widget | Qua inputs/outputs list |
Widget có thể tự bind vào key: st.text_input(key="x") |
Multi-page
Streamlit: native multi-page
Streamlit hỗ trợ multi-page từ v1.10 qua thư mục pages/ — Streamlit tự tạo sidebar navigation. Từ v1.36, st.navigation() cho phép cấu hình tường minh hơn:
my_app/
├── app.py
└── pages/
├── 01_Dashboard.py
└── 02_Chatbot.py
st.session_state persist khi user chuyển giữa các page — không cần truyền data qua query param hay database.
Gradio: không có "page" thực sự
Gradio có gr.TabbedInterface và gr.Tab bên trong gr.Blocks — nhưng đây là tab trong cùng một page, không phải routing giữa các URL khác nhau.
import gradio as gr
demo1 = gr.Interface(fn=classify, inputs="text", outputs="text")
demo2 = gr.Interface(fn=generate, inputs="text", outputs="text")
# Tab interface — cùng URL, chuyển qua tab
app = gr.TabbedInterface(
[demo1, demo2],
["Classifier", "Generator"],
)
app.launch()
Gradio không thay đổi URL khi chuyển tab. Nếu cần routing thực sự (/app/dashboard, /app/chatbot), phải mount vào FastAPI và tự xử lý routing ở tầng FastAPI.
Kết luận
Nếu app cần nhiều trang riêng biệt với URL và sidebar navigation, Streamlit là lựa chọn tự nhiên hơn. Gradio phù hợp khi các "trang" đơn giản có thể đặt trong tab.
Chatbot UI
Gradio: gr.ChatInterface — ít code nhất
gr.ChatInterface tự render message list, nút Retry, Undo, Clear và quản lý history. Chỉ cần viết hàm nhận message + history:
import gradio as gr
from openai import OpenAI
client = OpenAI()
def chat_stream(message: str, history: list):
messages = []
for user_msg, assistant_msg in history:
messages.append({"role": "user", "content": user_msg})
messages.append({"role": "assistant", "content": assistant_msg})
messages.append({"role": "user", "content": message})
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
stream=True,
)
partial = ""
for chunk in stream:
partial += chunk.choices[0].delta.content or ""
yield partial # streaming: yield token từng phần
gr.ChatInterface(fn=chat_stream).launch()
Toàn bộ chatbot với streaming trong ~20 dòng. History được Gradio quản lý — không cần session_state.
Streamlit: st.chat_input + tự quản history
Streamlit có st.chat_input, st.chat_message, st.write_stream nhưng phải tự quản lý history trong session_state:
import streamlit as st
from openai import OpenAI
@st.cache_resource
def get_client():
return OpenAI()
if "messages" not in st.session_state:
st.session_state.messages = []
# Render toàn bộ history mỗi lần rerun
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
if prompt := st.chat_input("Nhắn gì đó..."):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
with st.chat_message("assistant"):
stream = get_client().chat.completions.create(
model="gpt-4o-mini",
messages=st.session_state.messages,
stream=True,
)
response = st.write_stream(
chunk.choices[0].delta.content or ""
for chunk in stream
if chunk.choices[0].delta.content
)
st.session_state.messages.append({"role": "assistant", "content": response})
Khoảng 30 dòng — nhiều hơn Gradio, nhưng linh hoạt hơn: dễ thêm sidebar cấu hình, hiển thị nguồn RAG bên cạnh chat, tích hợp vào dashboard multi-page.
Khi nào chọn cái nào cho chatbot
- Chatbot thuần, standalone, deploy nhanh lên HF Spaces → Gradio gọn hơn.
- Chatbot là 1 trang trong dashboard lớn, cần sidebar filter, multi-page → Streamlit phù hợp hơn.
Deploy
Gradio
- Hugging Face Spaces (SDK = Gradio): native support — đây là deployment path được Gradio docs ưu tiên. Tỷ lệ lớn các Space trên HF là Gradio app. Push code lên repo Space là xong.
- Modal: hỗ trợ deploy Gradio app với GPU on-demand.
- Self-host với uvicorn: Gradio dùng ASGI (Starlette/uvicorn) nội bộ — có thể mount vào FastAPI app hiện có qua
gr.mount_gradio_app().
import gradio as gr
from fastapi import FastAPI
app = FastAPI()
gradio_app = gr.Interface(fn=predict, inputs="text", outputs="text")
# Gradio mount vào /demo, FastAPI vẫn chạy các route còn lại
app = gr.mount_gradio_app(app, gradio_app, path="/demo")
Streamlit
- Streamlit Community Cloud: link GitHub repo → deploy tự động. Free tier: 1 GB RAM, public repo.
- Hugging Face Spaces (SDK = Streamlit): cũng hỗ trợ, phù hợp khi cần GPU Space.
- Self-host: chạy
streamlit run app.py --server.port 8501 --server.headless truetrong container. Streamlit dùng Tornado (không phải ASGI) — không thể mount vào FastAPI.
# Docker entrypoint cho Streamlit
streamlit run app.py \
--server.port 8501 \
--server.headless true \
--server.address 0.0.0.0
Điểm khác biệt quan trọng về deploy
Gradio có thể chạy cùng process và cùng port với FastAPI API server — hữu ích khi bạn muốn team QA có UI test trong khi production traffic đi qua JSON API. Streamlit không có khả năng này vì dùng Tornado, không ASGI.
Khi nào chọn Gradio
- Demo 1 model cho người không-tech: PM, designer, client cần thử model mà không muốn gọi API.
gr.Interfacetạo UI rõ ràng với 5 dòng code. - Chatbot standalone:
gr.ChatInterfacevới streaming, retry, undo built-in — không cần viết boilerplate quản lý history. - Public demo nhanh trên Hugging Face Spaces: Gradio là SDK native của HF Spaces, quy trình push lên Space nhanh và không có friction.
- Mount cùng API FastAPI hiện có: thêm UI test cho team nội bộ mà không cần server riêng —
gr.mount_gradio_appcho phép Gradio chạy tại/demotrong khi FastAPI chạy các route còn lại. - App nhỏ, input → output rõ ràng: text classification, image segmentation, audio transcription, code generation — pattern form input/output của Gradio ăn khớp tự nhiên với dạng task này.
Khi nào chọn Streamlit
- Dashboard nhiều chart + filter + metric: sidebar filter, metric card ở trên, line chart ở giữa, dataframe ở dưới — Streamlit's vertical scroll + column layout phù hợp tự nhiên hơn.
- Internal tool multi-page: data exploration (trang 1), model monitoring (trang 2), chatbot (trang 3) — native multi-page với
pages/hoặcst.navigation(). - App có workflow tuần tự nhiều bước: upload file → cấu hình → preview → export. Vertical scroll của Streamlit thể hiện trình tự các bước rõ hơn.
- Plot phức tạp (Plotly, Altair, matplotlib): cả hai framework đều support, nhưng Streamlit tích hợp tự nhiên hơn —
st.plotly_chart(fig),st.altair_chart(chart),st.pyplot(fig)đều hoạt động không cần convert. - Chatbot tích hợp trong dashboard lớn: khi chatbot là 1 trong nhiều page, cần kết hợp với filter/context từ các page khác qua
session_state.
Khi không nên dùng cả hai
Cả Gradio và Streamlit đều là Python UI framework chạy single-process. Dưới đây là những trường hợp nên dùng công cụ khác.
Cần auth phức tạp (RBAC, SSO, OAuth)
Gradio có basic auth auth=(username, password) hoặc callback function — đủ cho team nội bộ nhỏ. Streamlit có st.secrets nhưng không có auth built-in cho SSO hay role-based access. Nếu cần phân quyền theo role (admin xem tab A, user thường chỉ xem tab B) hay đăng nhập qua Google/Azure AD — nên dùng React/Next.js + FastAPI backend với auth library đầy đủ.
Cần routing tùy chỉnh, SEO, SSR
Cả hai framework render phía server Python và không cung cấp SSR theo nghĩa web truyền thống, không có sitemap tự động, không có meta tag per-page. Nếu app cần indexable bởi search engine hoặc cần URL tùy chỉnh sâu — dùng web framework đầy đủ.
Bài toán scaling nhiều user đồng thời
Gradio và Streamlit đều chạy single-process Python. Gradio 4.x có queue tích hợp và concurrency limit cấu hình được (default_concurrency_limit), nhưng không tự horizontal scale. Streamlit tương tự — mỗi instance server là 1 process. Khi có hàng trăm concurrent user thường xuyên, cần cân nhắc:
- Scale bằng cách chạy nhiều instance đằng sau load balancer (nhưng session_state/gr.State không share giữa các instance — cần external store nếu cần share state).
- Hoặc chuyển sang FastAPI + React khi UI đủ phức tạp để đầu tư frontend riêng.
Cần custom JS sâu hoặc realtime WebSocket phức tạp
Gradio hỗ trợ gr.HTML và custom JS ở mức cơ bản. Streamlit hỗ trợ st.components.v1.html() để nhúng HTML/JS tùy chỉnh. Nhưng nếu UI yêu cầu logic JavaScript phức tạp (canvas, WebGL, realtime collaboration) — đây không phải territory của hai framework này.
Bảng tổng hợp
| Tiêu chí | Gradio 4.x | Streamlit 1.36+ |
|---|---|---|
| Execution model | Event-driven — chỉ chạy callback được gọi | Script rerun — toàn bộ file chạy lại mỗi interact |
| State management | gr.State per session, qua function args |
st.session_state dict, truy cập toàn cục |
| Layout mặc định | Form input/output (trái–phải) | Vertical scroll, sidebar, multi-column |
| Multi-page | Tab trong cùng page, không có routing | Native pages/ folder, st.navigation(), URL riêng |
| Chatbot built-in | gr.ChatInterface — retry, undo, clear có sẵn |
st.chat_input + tự quản history trong session_state |
| Demo 1 model (LOC) | ~5 dòng với gr.Interface |
~10–15 dòng |
| Dashboard nhiều chart (LOC) | ~60–80 dòng (Blocks + event) | ~30 dòng |
| Tích hợp FastAPI | Có — gr.mount_gradio_app(), ASGI |
Không — dùng Tornado riêng |
| Deploy native | HF Spaces (Gradio SDK), Modal, self-host uvicorn | Streamlit Community Cloud, HF Spaces (Streamlit SDK) |
| Auth built-in | Basic auth (tuple hoặc callback) | Không (cần custom hoặc third-party) |
| Plot libraries | Plotly, matplotlib qua gr.Plot |
Plotly, Altair, matplotlib, Vega — tích hợp tự nhiên hơn |
