Performance
Bytes on the page
Section titled “Bytes on the page”| File | gzip |
|---|---|
alacris.js (signals, templates, styles, elements) | ~5.97 kB |
live.js (the live client) | ~1.6 kB |
store.js (optional) | ~0.99 kB |
context.js (optional) | ~0.54 kB |
A page using the first two ships roughly 7.5 kB of framework, gzipped, and no hydration payload at all. The props are already in the HTML that was going to be sent anyway.
Alacris UI is a design system on top of that
runtime, not a second 6 kB library. Config.UI loads the whole catalog and
applies a theme; the payload is larger on purpose. There is no bundler step
that tree-shakes unused tags. That is the trade for one flag.
Bytes on the wire, after first paint
Section titled “Bytes on the wire, after first paint”One update to one list:
| Approach | Typical payload | Work in the browser |
|---|---|---|
| Full page reload | the whole document | full parse, layout, state lost |
| HTML fragment swap | the fragment’s markup | parse, diff or morph, DOM replaced |
| A prop patch | the changed value as JSON | one property write |
[{"o":"p","i":"board","k":"items","v":[...]}]There is no diffing step: a prop is a signal, so writing it wakes exactly the
bindings that read it. For a list, alacris’ each then reorders by key rather
than rebuilding.
Server cost
Section titled “Server cost”Per session: a small struct, a bounded patch buffer (256 entries by default),
and one goroutine only while a browser is attached. A patch is one
json.Marshal and one write to an already-open connection.
The SSE stream is a held connection, so the ceiling is file descriptors and memory rather than CPU. Budget roughly “open pages × what you queue for them”, and the buffer bound is what stops a disconnected tab growing without limit.
Batch anything that changes together: it is one frame, one
write, one paint.
Render cost
Section titled “Render cost”Rendering an element is string building. No reflection for string, bool,
int or float, which are handled by a type switch before any reflective path.
Composite props cost one json.Marshal.
Generated wrappers omit props equal to their defaults, so the HTML is smaller than a naive encoder would produce, and the saving compounds across a list.
Keep each out of conditionals
Section titled “Keep each out of conditionals”An each inside a conditional template rebuilds the entire list on every
change:
// Wrong: the thunk returns a fresh template result, so the ul and its each()// are rebuilt whenever items changes.${() => items().length === 0 ? html`<p>Nothing here.</p>` : html`<ul>${each(() => items(), row, (t) => t.id)}</ul>`}// Right: each() is created once. Emptiness toggles an attribute.<ul ?hidden=${() => items().length === 0}> ${each(() => items(), row, (t) => t.id)}</ul><p ?hidden=${() => items().length > 0}><slot name="empty">Nothing here.</slot></p>With the first version, a prop patch replaces every row node and drops focus,
exactly what the live layer exists to avoid. With the second, the same patch
updates the rows in place and the <li> elements are the same nodes afterwards.
The example app in the repository was written the wrong way first, and the browser test that checks node identity is what caught it.
Where the runtime is slow
Section titled “Where the runtime is slow”Creating DOM costs 3–4× hand-written DOM, because templates are parsed at runtime rather than compiled. Updating is competitive: removing a row is 0.075 ms against vanilla’s 0.010, and selecting one is at the floor.
The consequence for a Go server: rendering ten thousand rows into the initial
HTML and letting the component take them as one prop is fine, but a component
that builds ten thousand rows on the client will feel it. Paginate, or use
each with a store.
Full numbers, including the benchmark methodology, are in the alacris performance docs.
Practical checklist
Section titled “Practical checklist”- Set
Config.Versionso assets cache for a year instead of revalidating. Batchanything that changes together.- Prefer a prop patch to
SetHTML; it is smaller and does not touch the DOM. - Keep
eachout of conditionals. - Use
@prop {integer}where a value really is one.float64in Go for an id is a smell that usually ends in a rounding bug. - Serve your component module with a long cache and a hashed filename, the same as any other asset.