Performance
Why Mates stays fast and how to keep it that way — targeted DOM patches, keyed lists, memoization, and virtualization.
Why it's fast by default
Mates does not walk a virtual DOM. Templates track which atoms they read and patch exactly those bindings. The outer component function runs once, so per-update cost is the template expression itself — not a full component re-execution.
- No virtual DOM diffing — a toggle updates one text node, not a tree walk
- Two-layer components — setup once; render is cheap and targeted
- No compiler pass — nothing to mis-optimize; what you write is what runs
Numbers live on the Benchmarks page — keyed list ops vs React, Solid, Vue Vapor, and Angular (Mates plus Mates raw components), plus a 100k-todo full-DOM paint test vs Solid, Vue 3 (not Vapor), Angular 19.2, and React.
Key your lists
repeat() with a key function is the single biggest lever on large lists. Keyed rows are reused and patched in place; unkeyed renders rebuild DOM nodes:
import { html, repeat } from "mates";
return () => html`
<ul>
${repeat(
todos(),
(todo) => todo.id,
(todo) => html`<li>${todo.title}</li>`,
)}
</ul>
`;
Derive, don't recompute
Any value computed from other state belongs in memo() or a store getter — it recomputes only when its inputs change and stays out of the render path:
import { atom, memo, html } from "mates";
const todos = atom<Todo[]>([]);
const openCount = memo(() => todos().filter((t) => !t.done).length);
return () => html`<span>${openCount()} open</span>`;
- Do heavy work in the outer function or a memo — never inside the render function
- Read atoms once per template where possible; reading the same atom in ten bindings is ten subscriptions (cheap, but avoidable)
- Split big components so a state change re-renders a subtree, not the page
- Prefer derivation over
effectthat copies atom → atom — Patterns
Virtualize very large lists
Beyond a few thousand rows, don't render what you can't see. mates-virtual provides directives for lists, tables, masonry, and grids:
npm install mates-virtual
import { html } from "mates";
import { virtualList } from "mates-virtual";
return () => html`
${virtualList({
items: todos(),
renderItem: (todo) => html`<li>${todo.title}</li>`,
})}
`;
Exports: virtualList, virtualTable, virtualMasonry, masonryGrid. Package overview: Ecosystem.
Bundle size
- The full
matespackage is ~44KB gzipped including router, fetch, dates, CSS-in-JS, and WebSockets — one install, no per-feature packages to wire - Optional packages (
mates-virtual,mates-db,mates-ui) stay out of the bundle until you import them mates-iconsexports icons one-per-module — tree-shaking keeps unused icons out of production builds