Performance Analysis: Faultsense Agent v0.4.0
What the agent costs your page, tested at scale.
Summary
We measured the performance impact of the Faultsense agent across two benchmark suites: a demo benchmark (30 paired A/B measurements on an instrumented HTMX app) and a stress benchmark (15 paired measurements across 50, 200, and 1000 assertions with background DOM churn in a React 19 app).
Key findings:
- Zero INP impact at every scale tested, including 1000 assertions with 100 background mutations/second on a 4x CPU-throttled device.
- MutationObserver callback P99 is 2.2ms in the worst case (1000 assertions, CPU 4x throttle). The 50ms long-task threshold is never approached by a single callback.
- Heap overhead scales sub-linearly: ~76KB at 50 assertions, ~115KB at 200, ~140KB at 1000. The GC sweep cleans up resolved assertions.
- LCP impact is undetectable below 200 assertions. At 1000, the initial attribute scan adds 6-12ms (one compositor frame).
- The agent never creates a long task. At 1000 assertions, both conditions produce a long task from the heavy page itself; the agent adds ~23ms to the existing task during its one-time initial scan.
All results include Wilcoxon signed-rank p-values and 95% confidence intervals. Full data is in current.md (demo) and stress.md (stress).
Interaction to Next Paint (INP)
INP is the Core Web Vital that measures responsiveness. It is the metric most likely to be affected by a MutationObserver-based agent, because the observer callback runs on the main thread during DOM mutations triggered by user interactions.
| Scenario | INP Delta | p-value | 95% CI |
|---|---|---|---|
| Demo: active, unthrottled | 0ms | 0.807 | [-8ms, +8ms] |
| Demo: active, Slow 4G | +8ms | 0.262 | [+0ms, +8ms] |
| Stress: 1000 assertions, 100 churn/s, CPU 4x | 0ms | — | — |
The demo benchmark's p=0.807 is the strongest possible "no effect" signal — the confidence interval spans symmetrically around zero. Even under Slow 4G, the +8ms delta is not statistically significant (p=0.262).
The stress benchmark shows 0ms INP delta across all 12 configurations, including the worst case: 1000 assertions with 100 background mutations per second on a 4x CPU-throttled device.
MutationObserver Callback Scaling
The agent installs a single MutationObserver on document.body. Every DOM mutation on the entire page passes through this observer's callback. The stress benchmark measures callback duration directly by wrapping the MutationObserver constructor with performance.now() timing.
Unthrottled (Apple M4 Pro)
| Assertions | Background Churn | Callbacks | P50 | P95 | P99 |
|---|---|---|---|---|---|
| 50 | 0/s | 3,068 | 0ms | 0.3ms | 0.5ms |
| 50 | 100/s | 14,553 | 0ms | 0.1ms | 0.3ms |
| 200 | 0/s | 3,574 | 0ms | 0.3ms | 0.5ms |
| 200 | 100/s | 18,173 | 0ms | 0.1ms | 0.3ms |
| 1000 | 0/s | 5,247 | 0.1ms | 0.6ms | 0.8ms |
| 1000 | 100/s | 19,535 | 0.1ms | 0.2ms | 0.6ms |
CPU 4x Throttle (simulating mid-tier mobile)
| Assertions | Background Churn | Callbacks | P50 | P95 | P99 |
|---|---|---|---|---|---|
| 50 | 0/s | 3,057 | 0ms | 0.7ms | 1.1ms |
| 50 | 100/s | 8,632 | 0ms | 0.7ms | 1.0ms |
| 200 | 0/s | 3,547 | 0ms | 0.7ms | 1.1ms |
| 200 | 100/s | 14,365 | 0ms | 0.7ms | 0.9ms |
| 1000 | 0/s | 4,976 | 0.3ms | 1.1ms | 2.2ms |
| 1000 | 100/s | 14,953 | 0.4ms | 1.0ms | 1.4ms |
The worst-case P99 is 2.2ms (1000 assertions, CPU 4x, no churn). This is 4% of the 50ms long-task threshold. The callback never comes close to blocking the main thread.
Background churn (non-instrumented DOM mutations) does not degrade callback performance. In most configurations, churn improves P95 — the MutationObserver batches mutations, and more frequent small batches are cheaper to process than fewer large ones.
Heap Overhead
| Assertions | Churn | Heap Delta (unthrottled) | Heap Delta (CPU 4x) |
|---|---|---|---|
| Idle (demo) | — | +1.7KB | — |
| 50 | 0/s | +76KB | +69KB |
| 50 | 100/s | +92KB | +82KB |
| 200 | 0/s | +115KB | +120KB |
| 200 | 100/s | +150KB | +152KB |
| 1000 | 0/s | +116KB | +117KB |
| 1000 | 100/s | +140KB | +102KB |
Heap scales sub-linearly. Going from 50 to 1000 assertions (20x) increases heap by roughly 1.5-1.8x. The agent's internal GC sweep (every 5 seconds) cleans up resolved assertions, keeping the working set bounded.
At idle (no assertions firing), the agent's footprint is 1.7KB (p=0.021, CI [+1.5KB, +1.7KB]) — just the observer, listeners, and sweep timer.
First Paint Impact
| Assertions | LCP Delta (unthrottled) | LCP Delta (CPU 4x) | p-value |
|---|---|---|---|
| Demo (active) | -4ms | — | 0.057 (noise) |
| 50 | -6ms | 0ms | 0.025 / 0.415 |
| 200 | -4ms | -20ms | 0.008 / 0.001 |
| 1000 | +6ms | +8ms | 0.006 / 0.092 |
Below 200 assertions, LCP deltas are negative (measurement noise — the agent doesn't make pages faster). At 1000 assertions, there is a statistically significant LCP increase of +6ms (p=0.006, CI [+4ms, +12ms]). This is the one-time cost of scanning 1000 fs-* attributes during initial page load — roughly one compositor frame.
For context: a typical instrumented page has 10-50 assertions. The 1000-assertion test is a deliberately extreme stress case.
Long Task Analysis
The agent never creates a new long task in any scenario tested. At 1000 assertions, the page itself produces a long task from rendering 3,696+ DOM nodes. The agent adds ~23ms to this existing long task during its initial attribute scan:
| Scenario | Without Agent | With Agent | Agent Contribution |
|---|---|---|---|
| 1000 assertions, unthrottled | 67ms | 90ms | +23ms |
| 1000 assertions, 100 churn/s | 71.5ms | 95ms | +23.5ms |
| 1000 assertions, CPU 4x | 119ms | 90ms | -29ms (noise) |
This is a one-time cost at page load, not ongoing overhead. During steady-state operation (after initial scan), the agent produces zero long tasks across all configurations.
Measurement Validity
A-vs-A Baseline
The stress suite includes an A-vs-A baseline (both conditions run without the agent) to validate measurement stability:
| Metric | Delta | p-value | Interpretation |
|---|---|---|---|
| LCP | 0ms | 0.351 | Apparatus is stable |
| Heap | +16 bytes | 0.875 | No measurement bias |
| TTFB | -0.35ms | 0.002 | Ordering bias exists |
The LCP and heap baselines confirm the apparatus does not create false positives. The TTFB ordering bias (condition A consistently shows higher TTFB than condition A2) is a known artifact of the A-then-B measurement order and affects all TTFB comparisons in the report. TTFB is excluded from our summary findings for this reason.
Statistical Method
All significance claims use the Wilcoxon signed-rank test (two-tailed, non-parametric) on paired A-B differences. Confidence intervals use the Hodges-Lehmann estimator over Walsh averages. The demo suite uses 29 usable pairs; the stress suite uses 14 usable pairs per configuration. Thresholds: p < 0.01 = significant, 0.01 <= p < 0.05 = measurable, p >= 0.05 = within noise.
Reproduction
All benchmarks are reproducible from the open-source agent repository:
# Demo benchmark (30 pairs, ~2 hours)
npm run benchmark:demo
# Stress benchmark (15 pairs, ~4 hours)
cd tools/benchmark/stress && npm install && cd ../../..
npm run benchmark:stress
# A-vs-A measurement validation
npm run benchmark:ava
Environment: Apple M4 Pro (14 cores), 48GB RAM, Chromium 147.0.7727.15, Node v22.21.0. Run on a quiet machine with no background processes for comparable results.
Stress Harness Architecture
The stress benchmark uses a React 19 application with configurable assertion density and background DOM churn. Assertions cycle through 8 archetypes that exercise all resolver code paths:
| Archetype | Trigger | Assertion Type | Share |
|---|---|---|---|
| Click -> Updated | click | fs-assert-updated with text-matches |
20% |
| Click -> Added | click | fs-assert-added |
15% |
| Click -> Removed | click | fs-assert-removed |
10% |
| Input -> Visible | input | fs-assert-visible with value-matches |
10% |
| Submit -> Conditional | submit | fs-assert-added-success/error with mutex |
10% |
| Mount -> Visible | mount | fs-assert-visible with classlist |
10% |
| Invariant -> Stable | invariant | fs-assert-stable |
10% |
| Click -> OOB Chain | click | fs-assert-updated with OOB reference |
15% |
Background churn is generated by non-instrumented React components that toggle classes, update text, and add/remove child nodes on a timer — simulating third-party widgets, animations, and other DOM activity the agent must filter through.