Performance
WakaPAC is designed around one core insight: DOM manipulation is expensive; everything else is cheap. Its internal architecture is built to do as little DOM work as possible, batch what it can't avoid, and never pay for something twice. This page covers the specific techniques that keep WakaPAC applications fast.
Expression Caching
Every binding expression in your HTML like data-pac-bind="enable: count > 0", has to be parsed from
a string into an AST before it can be evaluated. That parsing is not free, and in a reactive application the
same expression can be evaluated on every state change.
WakaPAC uses a dedicated ExpressionCache that stores the parsed result of every expression
and binding string the first time it is seen. Subsequent evaluations skip straight to execution
against the current data. The cache covers both interpolated expressions ({{ … }}) and
full binding strings (data-pac-bind="…"), using the raw string as the key after whitespace
normalization so equivalent expressions always hit the same slot.
Cache size is bounded at 1000 entries. When that limit is reached, the oldest entry is evicted before inserting the new one — a simple FIFO strategy that prevents unbounded memory growth in applications.
DOM Write Diffing
Reactive updates fire whenever data changes, but an update does not mean a DOM write. Before touching the DOM, WakaPAC compares the newly computed value against the last value it wrote to that binding and skips the write entirely if they are equal.
The comparison uses a structural deep equality check — not reference equality — so it handles
objects and arrays correctly. The fast path is a strict === check that returns
immediately for primitives. Only when that fails does it fall into recursive key-by-key comparison,
and even then it short-circuits as soon as a difference is found. Previous values are stored
directly on each element, keeping lookup O(1) with no secondary data structure.
Scoped Binding Maps
When a component is initialized, WakaPAC scans its DOM subtree once and builds two maps: an
interpolation map that indexes every element with bindings, and a separate set of
text node records for {{ … }} interpolations. All subsequent reactive updates
iterate these maps directly — there is no re-querying of the DOM and no walking of the full
document tree. Only nodes that belong to the component are ever visited.
Within each element's binding loop, the scope resolver (which handles foreach item aliasing)
is constructed once per element, not once per binding key. Binding keys are iterated with
a plain for loop rather than array methods to avoid a closure allocation on every
iteration, and the two most common non-applicable binding types (foreach and
click) are filtered with direct equality checks rather than Array.includes().
Batch DOM Rendering for Lists
When a foreach binding re-renders — because an array was mutated — WakaPAC builds
the complete HTML string for all list items first, then sets innerHTML once.
This produces a single layout reflow instead of one per item, which matters significantly for
large lists.
Partial templates referenced inside a foreach block are expanded once before the
item loop, not once per item. Because partials are static markup, they never need to be
re-expanded per iteration.
Event Delegation
Browser event listeners are attached once, at the document level, not per component
or per element. A single handler for each event type (mousedown, keydown, input, change,
drag events, and so on) receives every matching event from the entire page and routes it to
the correct component by walking up the DOM to find the nearest [data-pac-id]
ancestor. This means the number of registered event listeners stays constant regardless of
how many components or elements are on the page.
Click handlers declared via data-pac-bind="click: method" follow the same model —
they are not wired individually per element.
High-Frequency Event Coalescing
Events that fire continuously — mousemove, touchmove, scroll, and resize
— are throttled through a requestAnimationFrame-based coalescer. Instead of
dispatching every raw DOM event, WakaPAC stores only the most recent event in a slot and
fires it at most once per animation frame. Only one rAF callback is ever queued
at a time; any subsequent events arriving before the next frame simply overwrite the pending
slot. This ensures mousemove and touchmove never dispatch faster than the display refresh rate,
and an optional FPS cap can reduce delivery further (the default for mousemove is configurable
via wakaPAC.mouseMoveThrottleFps).
Window scroll and resize events are routed through the same coalescer at 60 fps. Container
scroll state is similarly debounced with a requestAnimationFrame to absorb
rapid scroll bursts without triggering a reactive update on every pixel.
Selective Reactivity
WakaPAC wraps your component data in a deep Proxy that intercepts reads and writes
to trigger DOM updates. Not every property needs to participate in this system. Properties
whose names start with _ or $ are excluded from the reactive proxy —
they are stored and read as plain values. This convention exists precisely for cases where you
need to attach DOM references, internal state, or complex objects to the abstraction without
the overhead of wrapping them in a proxy and potentially triggering spurious updates.
Deferred Input Updates
Input bindings support three update triggers: immediate (default), delay: N,
and change (on blur). The delay and change modes defer
writing user input back into the abstraction, which defers all downstream reactive updates.
A single setTimeout manages all pending delay-triggered updates; if a faster-firing
entry arrives it replaces the scheduled time rather than stacking new timers.
Shared Browser Observers
Viewport visibility and element size are tracked via a single shared IntersectionObserver
and a single shared ResizeObserver for the entire WakaPAC runtime. Each component
registers its container element with these shared observers rather than instantiating its own.
Observer callbacks receive all notifications from a frame in a single batch and process them
in one pass.
One-Time Static Analysis
Several pieces of work that would otherwise repeat on every update are done once and stored.
Module-level lookup tables (VK_MAP for virtual key codes, KEY_MODIFIER_MAP
for modifier names) are allocated when the script loads, not on each event. The reverse mapping
from key codes to human-readable names is built lazily on first use and then cached for the
lifetime of the page. Partial template content is collected from the document exactly once on
the first wakaPAC() call.
Component hierarchy — the parent/child relationships between nested components — is built in a single O(n) DOM traversal after initialization, using a pre-constructed element-to-component index for constant-time parent lookup. Rapid registrations (such as a parent dynamically creating children) are batched with a 10 ms coalesce window before hierarchy resolution runs.
Paint Queue
For components that use the canvas API via the MSG_PAINT message, dirty regions
are accumulated in a map and flushed in a single requestAnimationFrame callback.
Queuing an invalidation when a flush is already scheduled is a no-op — there is never more
than one pending frame. Sub-regions queued for the same component are collected into a region
list, and getDC() clips rendering to that entire region so only genuinely dirty
area is drawn.