stario-logo

The Storyboard Approach

Web development is schizophrenic: the browser shows something, the server has something else. Nobody agrees on truth. So you layer in state managers, APIs, sync logic, derived state - a whole civilization to manage disagreement.

What if there was no disagreement?

Stario + Datastar flip it: the backend is canonical. It renders complete HTML and streams it. Not JSON. Not patches. The actual thing the user sees, rendered fresh, right now.

This mirrors the "fat-morph" approach - where complete state snapshots replace partial updates and keep client/server in perfect sync.

Think storyboarding: each frame shows the complete scene. Login page. Form submitted. Error. Dashboard. Not "send me the token, I'll update these 12 things." Just: here's what the screen looks like.

The Core Contradiction

Traditional: "Send me data, I'll figure out what to show."

  • POST /login{"user_id": 42}
  • Now frontend must: set token, update nav, redirect, invalidate cache, sync local state...
  • Each step is a failure point.
  • What if one step is missed? You've got divergence - until the user refreshes or clears cache.

Stario: "Here's what the screen looks like now."

@app.query("/screen")
async def screen(ds: Datastar):
    while True:
        await state_changed.wait()
        yield render_page(current_state, ds)  # Complete. Fresh. Done.

One connection. One truth. Backend renders, browser displays.

The "Wasteful" Myth

"Complete HTML every time is wasteful." - FALSE! 🚀

# Bandwidth comparison (single session, 50 interactions)

**Streaming (Stario + Brotli):**
- Frame 1: 150KB → 8KB compressed (94% compression)
- Frames 2-50: ~1-2KB each (same Brotli dictionary grows)
- **Total: ~75KB**

*Assumes: persistent connection, repeated HTML patterns, Brotli enabled*

**Traditional REST (per request):**
- Request overhead: 1-3KB each
- JSON payload: 15-20KB each  
- **Total: 800KB-1.2MB for 50 requests**

*Assumes: no HTTP/2 server push, no response caching*

Brotli's dictionary grows with repeated content, making long-lived connections exponentially more efficient. This efficiency compounds - it's a key argument for single-connection architectures over request-reply.

Pattern in Practice

from stario import Stario
from stario.datastar import Datastar, Signal
from stario.html import div, h1, ul, li, input_, button, render

app = Stario()
state = {"todos": []}
state_changed = asyncio.Event()

# ONE streaming query: complete state projection
@app.query("/screen")
async def screen(ds: Datastar):
    while True:
        await state_changed.wait()  # Pauses until state changes
        yield render_page(state, ds)

# Commands: modify state, notify query
@app.command("/add")
async def add(text: Signal[str], ds: Datastar):
    state["todos"].append({"text": text, "done": False})
    state_changed.set()      # Notify query to re-render
    state_changed.clear()    # Reset for next update

def render_page(state, ds):
    """The complete scene, rendered fresh"""
    todo_items = [li(todo["text"]) for todo in state["todos"]]

    return div(
        {"id": "app"},
        h1("Todos"),
        ul(todo_items),
        div(
            {"class": "input-group"},
            ds.signals({"text": ""}),
            input_(
                {"type": "text", "placeholder": "Add a todo..."},
                ds.bind("text"),
            ),
            button(
                ds.fetch_on("click", "add", method="post"),
                "Add",
            ),
        ),
    )

Flow: Browser connects → Server renders state → User clicks → Command modifies state → Query re-renders → Browser morphs DOM [via Datastar] → Loop.

No sync layer. No derived state. No coordination.

Honest Assessment

🚀 Where this really shines:

  • Real-time collaboration (documents, dashboards, live feeds)
  • High-frequency interactions (single connection beats polling)
  • Complex interdependencies (render them together)
  • Backend-strong teams (Python > JavaScript)

⚠️ When to consider alternatives:

  • Public APIs (clients are external)
  • Offline-first (needs different strategy)
  • Heavy client logic (games, audio editors)
Scenario Recommendation
Massive pages? Cache components server-side, lazy load, paginate.
Rapid inputs? Debounce client-side.
SPAs? Mix Datastar signals with vanilla JS.
Offline? Offline-first apps require different architecture. Stario uses SSE, which can resume briefly-dropped connections but isn't designed for extended offline use.

The Shift

Stop thinking "what changed?" Think "what does the screen look like now?"

Backend renders complete state. Browser displays it. They agree. Always.

One connection. One truth. No cache nightmares. No stale data. No sync bugs.

Just render. The schizophrenia ends.

stario-logo