Performance
Performance¶
The big idea¶
Wybthon component bodies run once. Reactive updates flow through reactive holes: a per-binding effect for each callable child or prop value. When a signal changes, only the holes that read it re-evaluate, and only the affected DOM nodes are touched. This is fundamentally cheaper than a full component re-render plus diff, even though we still use a VDOM internally to batch DOM mutations across the Python and JS bridge.
Authoring tips¶
- Prefer holes over re-rendering. Embed a signal accessor (or
dynamic(lambda: ...)) at the smallest possible spot in the tree rather than reading signals eagerly inside the component body.
# Slow: eager reads happen during setup; future changes are missed.
return p(f"Hello, {name()}, count={count()}")
# Fast: only two text nodes update.
return p("Hello, ", span(name), ", count=", span(count))
-
Co-locate signals with the smallest visible region. A hole is cheaper than a
Show/Forre-evaluation, and aFor/Indexper-item scope is cheaper than adynamicreturning the full list. -
Use
create_memofor derived values. Memoised getters work out-of-the-box as reactive holes:span(my_memo). -
Use
keyon lists. Keyed reconciliation reorders existing DOM nodes in place instead of re-creating them. Prefer stable IDs over indices for keys. -
Batch updates with
batch(). When a single user action triggers multipleset()calls, wrap them inbatch()so all affected holes flush together. -
Use
For/Indexfor dynamic lists. These maintain stable per-item (or per-index) reactive scopes so the mapping callback runs once per unique item, not per re-render.
Micro-benchmarking¶
Run the included benchmarks against the stubbed DOM:
The runner measures:
- Standard JS framework benchmark workloads (create, update, swap, and remove rows in a 1k to 10k row table); useful as a regression smoke test for the diffing algorithm.
hole update (1k tree): change one signal that drives a single reactive hole inside a 1,000-node tree.full rerender (1k tree): re-render the entire tree and let the diffing algorithm reduce the change set to one text node.
Both tree benchmarks update the same DOM node; the difference is entirely in what work the framework does to figure out the change. On the stubbed DOM (which makes every framework cost more visible than real browser DOM mutations) the gap is dramatic:
| Benchmark | Mean |
|---|---|
hole update (1k tree) |
~0.01 ms |
full rerender (1k tree) |
~25 ms |
In real Pyodide deployments the absolute numbers shift, but the relative ordering is the same: skipping the diff entirely is always a win.
Use --bench=<name> to run a single benchmark or --json to emit
machine-readable output.
Next steps¶
- Read Mental model for the underlying ideas.
- Browse Authoring patterns for hole-friendly recipes.
- See Suspense and Lazy Loading for code-splitting.