When building FastHTML apps that need to display multiple components on a page, the traditional (at least the first way I tried!) approach is to have a single route handler that computes and returns all components:
@app.get("/dashboard")
def dashboard():
# Potentially slow computations
profile_data = compute_profile_data() # Fast: 100ms
activity_data = compute_activity_data() # Medium: 500ms
analytics_data = compute_analytics_data() # Slow: 2000ms
return Titled("Dashboard",
profile_component(profile_data),
activity_component(activity_data),
analytics_component(analytics_data)
)
The bottleneck issue: The page load time is the sum of each of the components because they are running in series. For example, if there is one really slow component, the entire page load is blocked by that component. But what if the user can get each component as soon as it's ready?
Instead of a single route that returns everything, we can use HTMX to trigger multiple requests in parallel and update different parts of the page as responses arrive.
Key HTMX attributes:
hx-trigger
: Specifies what event triggers the request and from wherehx-include
: Specifies which form inputs to include in the requestimport time
from fasthtml.common import *
app, _ = fast_app()
@app.get("/")
def home():
return (
Title("Multi-Route Form Example"),
# Form with fields
Form(
id="main-form",
)(
Fieldset(
Label("Your Name", Input(type="text", name="username", id="username")),
Label("Message", Textarea(name="message", id="message")),
),
),
# Button outside the form
Button("Update All", id="trigger-button"),
# Container for components
Div(
# Component 1 - Fast
Div(
hx_get="/greeting",
hx_trigger="click from:#trigger-button",
hx_include="#username",
hx_swap="innerHTML",
id="greeting-component",
)(
H3("Personal Greeting"),
P("..."),
),
# Component 2 - Medium
Div(
hx_post="/echo",
hx_trigger="click from:#trigger-button",
hx_include="#username, #message",
hx_swap="innerHTML",
id="echo-component",
)(
H3("Message Echo"),
P("..."),
),
# Component 3 - Slow
Div(
hx_get="/time",
hx_trigger="click from:#trigger-button",
hx_swap="innerHTML",
id="time-component",
)(
H3("Current Time (with delay)"),
P("..."),
),
),
)
# Fast endpoint (~100ms)
@app.get("/greeting")
def greeting(username: str = None):
return Div(H3("Personal Greeting"), P(f"Hello, {username or 'Anonymous'}!"))
# Medium endpoint (~500ms)
@app.post("/echo")
def echo(username: str = None, message: str = None):
time.sleep(0.5) # Simulate processing time
return Div(
H3("Message Echo"),
P(f"From: {username or 'Anonymous'}"),
P("Message: " + (message or "No message provided")),
)
# Slow endpoint (~2000ms)
@app.get("/time")
def current_time():
time.sleep(2) # Simulate slow processing
current_time = time.strftime("%H:%M:%S")
return Div(
H3("Current Time (with delay)"),
P(f"Server time: {current_time}"),
P("This component took 2 seconds to load"),
)
serve()
You can paste the above code and run it directly if you have FastHTML installed in your environment. Check out the network tab to see multiple requests firing.
hx-include
Each component div needs:
hx-trigger="click from:#trigger-button"
- Listen for button clickshx-include="#fieldId"
- Include specific form fields in the requesthx-get
or hx-post
- Specify the endpointhx-swap="innerHTML"
- Replace the component's content with the responseEach route handles only what it needs:
/greeting
- Only needs username, returns quickly/echo
- Needs username and message, medium processing time/time
- Doesn't need any form data, slow processing timetime.sleep()
hx-indicator
if you want loading indicators for each componenthx-trigger="every:5s"
to components that need regular updates