Performance

Tama performance work starts from a different baseline than React. Components run once, and observable updates patch the DOM directly instead of triggering a rerender pass for the whole component.

That changes which optimizations matter.

The First Rule

Measure before optimizing.

The most common Tama performance issues are not "too many rerenders". They are usually:

  • replacing large observable lists too often
  • keeping too much data in one state object
  • moving cached DOM unintentionally by reusing the same JSX object in multiple places
  • doing expensive work in subscriptions that should live in derived state or model classes

Use State Granularity Intentionally

Prefer state shaped around independent updates.

Good

const name = new State("")
const email = new State("")
const pending = new State(false)

Heavier Than Needed

const form = new State({
  name: "",
  email: "",
  pending: false,
})

The second pattern still works, but every whole-object replacement can trigger more downstream work than necessary.

If you do keep grouped state, prefer property selectors such as state.$.field or derived state with .to(...).

Derived State Beats Repeated Imperative Work

If a value depends on other reactive values, compute it once as derived state instead of recalculating it from multiple subscriptions.

const subtotal = new State(50)
const tax = new State(10)

const total = State.combine([subtotal, tax], (subtotal, tax) => subtotal + tax)

This keeps update paths explicit and easier to test.

Timing Sacrifice

If a computed value or side-effect depends on multiple sources, it may be unintentionally triggered multiple times during updates.

In case if recieved updates cause unrelated effects or negatively impact performance, you can use State.batch to defer updates until the end of the batch.

const total = State.combine([subtotal, tax], (subtotal, tax) => subtotal + tax)

Lists Are The Main Performance Boundary

Tama reconciles iterable updates positionally, without keys: identical items (===) keep their DOM nodes untouched, changed items are patched in place, and only shape mismatches rebuild a position.

That means list performance depends heavily on how you model identity — unchanged item objects are what make updates near-free.

Attach A StateReconciler For Large Or Churning Lists

const products = new StateArray<Product>([])
products.reconciler = StateReconciler.IDENTITY

const rows = products.map(product => <ProductRow product={product} />)

The reconciler makes map reuse derived values of items that are "the same" across updates, so plain map without one (which re-runs its callback for every item on every update) is only worth it for small lists.

Prefer StateArray For Mutable Collections

const products = new StateArray<Product>([])

Keep Row Identity In Model Objects

class ProductRowModel {
  constructor(
    readonly id: string,
    readonly title: State<string>,
    readonly price: State<number>,
  ) {}
}

const rows = new StateArray<ProductRowModel>([])

Avoid Replacing Huge Lists For Tiny Item Changes

If only one item changed, prefer changing the state inside that item model instead of rebuilding the whole rendered list from plain objects.

Reuse JSX Cache Intentionally

WebInflator caches inflated output per JSX object identity. That is useful for repeated view swaps and pre-inflated layouts.

const loadingView = <div>Loading...div>

function Panel(this: Tama.Component) {
  this.view.set(loadingView)
}

But the same mechanism can surprise you if you reuse one JSX object in multiple places at the same time. The cached node is the same DOM node, so attaching it elsewhere moves it away.

Good Use

  • swapping between a small number of repeated views
  • pre-inflating a layout used by one active branch at a time

Risky Use

  • returning the exact same JSX object from multiple concurrently mounted components

If you need distinct DOM instances, create fresh JSX values instead of reusing one cached object.

Pre-Inflate When Swapping The Same Views Repeatedly

If a shell toggles between a few stable layouts, pre-inflating them can reduce repeated inflation work.

function Shell(this: Tama.Component) {
  const summaryView = this.inflator.inflate(<section>Summarysection>)
  const detailsView = this.inflator.inflate(<section>Detailssection>)

  this.view.set(summaryView)
}

Use this only when the same view instances are intentionally reused.

Keep Expensive Side Effects Out Of Render Wiring

Tama components run once, so you can create functions freely inside them. The performance risk is usually not function allocation. It is doing heavy work inside subscriptions or event handlers that should live in:

  • derived state
  • store classes
  • explicit lifecycle routines

Mount Only What Matters

Conditional mounting is one of the easiest wins. If a large subtree is not needed, keep it unmounted instead of hidden.

<aside mounted={sidebarVisible}>...aside>

This avoids keeping expensive DOM and lifecycle work alive unnecessarily.

Validate Performance With Focused Tests

For performance-sensitive features, add tests for the behavior that matters:

  • list replacement boundaries
  • async view transitions between loading and ready states
  • mount and unmount behavior for expensive regions

Then benchmark separately when the numbers matter.

Practical Checklist

  1. keep frequently changing values in granular state
  2. use derived state instead of duplicated subscription logic
  3. model list item identity explicitly
  4. reuse cached JSX only when you really want the same node instance
  5. unmount large inactive regions instead of just hiding them

Related guides: