Skip to content

Performance

Generative DOM is designed so that performance optimizations are architectural rather than micro-optimizations. This guide covers the built-in performance features.

requestAnimationFrame Scheduling

push() does not render immediately. Instead, it marks the buffer as dirty and lets the scheduler handle it. The scheduler uses requestAnimationFrame to batch updates, ensuring that rapid push() calls result in a single render per frame.

This means 100 calls to push() within a single frame produce one DOM update, not 100.

Debouncing

The debounceMs option sets a minimum interval between renders. The default of 16ms (~60fps) prevents the renderer from running more than once per frame. Increase this value for lower CPU usage:

ts
const md = new GenerativeDom({
  container: el,
  plugins: [...],
  debounceMs: 50, // render at most 20 times per second
});

Object Pool

Generative DOM maintains a pool of DOM elements keyed by tag name. When elements are removed during AST diffing, they are returned to the pool instead of being discarded. When new elements of the same type are needed, they are drawn from the pool instead of being created.

This reduces garbage collection pressure during rapid streaming, where content is constantly being added and updated.

ts
// Check pool statistics
const stats = md.pool.stats();
console.log(stats);
// { pooled: 12, active: 45, created: 57, reused: 23 }

The pool stores up to 100 elements per tag name. Elements beyond this limit are garbage-collected normally.

Pool Lifecycle

  1. Acquire: Plugin calls ctx.createElement('p'). Pool checks for a recycled <p>. If found, returns it. Otherwise, creates a new one.
  2. Release: When a token is removed from the AST, its DOM element is released. The pool clears all attributes, children, and event listeners, then stores it.
  3. Drain: md.destroy() calls pool.drain(), releasing all pooled elements for garbage collection.

Incremental Parsing

The tokenizer maintains a cursor into the buffer. On each render cycle, parsing starts from the cursor -- not from the beginning. Only new content is tokenized. This makes push() an O(new content) operation, not O(total content).

AST Diffing

The renderer diffs the new AST against the previous one. Only changed, added, or removed tokens result in DOM mutations. If a token's raw value has not changed since the last render, its DOM node is left untouched.

Performance Tips

  • Choose the right debounce. For LLM streaming (tokens arrive every 20-50ms), the default 16ms works well. For bulk loading, consider a higher value.
  • Use only the plugins you need. Each plugin adds processing time to the tokenization loop. If you only need headings and paragraphs, do not load the full plugin set.
  • Call flush() once at the end. Do not call flush() after every push() -- that defeats the purpose of debouncing.
  • Reuse instances with reset(). Instead of destroy() + new GenerativeDom(), call reset() to clear the output while keeping plugins initialized and the pool warm.
  • Monitor pool stats in development. If reused is zero, something is preventing element recycling.