Performance and bundle boundaries
Interpret Coaction benchmarks honestly and keep local applications on the transport-free entry point.
Performance claims are useful only when the scenario, runtime, state shape, and package versions are visible. Coaction keeps its benchmark scripts in the repository so results can be regenerated instead of treated as timeless numbers.
Choose the local entry point
For a vanilla local store:
import { create } from 'coaction';This static boundary excludes shared protocol, epoch, reconnect, and transport modules from the
entry. Use coaction/shared for the shared runtime, or @coaction/react/shared in React.
The default React entry, @coaction/react, also stays local.
Bundle budgets in the repository measure published entries and tree-shaken consumer fixtures. They are regression gates, not a promise that an application bundle equals the budget number; the final result also depends on external dependencies retained by the application's bundler.
Run the maintained checks
pnpm build
pnpm package:size
pnpm benchmark
pnpm benchmark:zustand-positioning
pnpm benchmark:checkpackage:sizechecks published entry budgets and core entry isolation.benchmarkcompares large update paths, including Coaction draft updates and Zustand + Immer.benchmark:zustand-positioningseparates stable derived reads from update-then-read work.benchmark:checkenforces regression floors with CI headroom.benchmark:derivedruns built-entry workloads in fresh processes: sparse paths, related/unrelated writes, full scans, cached reads, and unread/disposed owners. It is included inbenchmark:check; full deep scans are reported to make their tracking cost visible, while native getters provide the coarse scan baseline.
Thresholds must not be lowered merely to make a failing run green. A threshold change needs a reviewed runtime-semantic or benchmark-methodology reason.
Understand the derived-state tradeoff
Cached accessors make repeated stable reads extremely cheap. After an update, Coaction still must preserve the readonly public-state boundary and update the snapshot used by computed traversal. That makes “update then read a derived value” a different path from a stable cached read.
Leave enablePatches unset unless an integration explicitly needs a journal. Commit hooks,
shared transport and some tracking paths request patches automatically. Eligible small read
sets can instead compare observed values before and after a write without generating patches;
cached getter snapshots, patch middleware and larger dependency sets retain the patch path.
Ordinary subscribe() listeners do not request patches themselves.
Do not publish one of those cases as a universal “faster than Zustand” claim. Zustand can make a derived read constant-time by storing the derived value, but application actions must then keep it consistent. Coaction's proposition is maintained cached derivation, not winning every mutation microbenchmark.
Render performance
Both useStore(selector) and observer() use managed deep path tracking on native immutable
stores. A reader of state.user.name can stay cached after an age-only recipe update. Selectors
still compare their results with Object.is; observer() collects reads during render without
requiring a selector. Measure selector executions and render counts separately.
Native getters and get(deps, fn) retain the store/slice field boundary. A getter reading
this.user.name can recompute when user.age changes, even if its result stays equal and its
consumers do no extra work. Use derive(..., { deep: true }) or derivePath when a managed
derived read needs finer dependencies.
Parent replacement, array structure changes and graph snapshot patches can invalidate more broadly. External adapters follow their own update and snapshot contracts. See computed tracking granularity for the boundaries.
Practical checklist
- Import
coactionfor vanilla local-only state. - Leave
enablePatchesunset unless needed; account for consumers that request patches automatically. - Use cached getters for repeated derived reads, not side-effectful work.
- Use
observer()where it removes real selector composition. - Profile production builds and representative state, not development mode alone.
- Treat benchmark floors as regression detection and rerun publishable numbers on the target machine.