Skip to content

Optimistic UI and fast lists

Add .optimistic to a DOM event to mark its target pending synchronously. Add one or more .optimistic-class-<name> modifiers to predict presentation classes:

<button @click.optimistic.optimistic-class-bg-emerald-600={toggle_item(item.id)}>
{item.name}
</button>

On dispatch, PyWire applies data-pw-pending and the declared classes before the next frame, and guards against a duplicate submit. The server remains the only source of business logic. The arriving morphdom patch is the reconciler: a correct prediction produces no visible change, while a rejected prediction is removed automatically. Inputs remain user-owned during the round-trip.

Do not use optimistic UI for:

  • actions whose visual prediction cannot be expressed as a pending attribute/class;
  • operations where duplicate prevention is a security boundary;
  • per-token output or arbitrary progress that requires server push.

Give every loop a stable, unique key:

<ul>
<li $for={item in items} $key={item.id}>
{item.name}
<button @click={toggle_item(item.id)}>{item.done}</button>
</li>
</ul>

PyWire creates a keyed region for each iteration. Mutating one item re-renders and ships only that row. In the measured Cloudflare workerd gate, toggling one row in a 1000-row list produced a 251 B update: O(n) list content collapsed to O(1) payload per item mutation.

Keys must be stable and unique. Duplicate or churning keys cannot safely identify DOM regions.

Each item’s region normally sits on a wrapper <div style="display: contents">. Table rows, cells and <option>s can’t be wrapped that way, because the HTML parser moves the <div> out of a table and drops it from a <select>. There the region goes on the item’s own element instead, so a keyed loop in a table or <select> must render exactly one <tr>, <td> or <option> per item. Anything else is a compile error.

Append, remove, reorder, or fully reassign the collection falls back to a whole-loop render. This is intentional v1 behavior, not a per-row incremental update. For interactions that repeatedly insert or reorder, expect the larger payload until structural keyed regions ship.

$key still improves identity tracking on stateful transports too; the same render_update shape is used by WebSocket, HTTP-session, and stateless transports.