Where to draw the line? The answer is "further than you'd think." Validation errors? data-error attributes or aria-invalid on the input. Derived values? Compute them from DOM on demand rather than caching. Server-driven defaults? Server renders them INTO the DOM, then DOM is truth.
The key rule of thumb: if it affects what the user sees, it belongs in the DOM. If you're putting state in JS just to render it back to the DOM, you've created a sync problem. If it's data persistance or state that only code cares about... back-end, pure function, using some efficient memory structure.
SSR consistency is actually easier this way. Server renders HTML. Client reads it. No hydration mismatch because there's nothing to reconcile.
MutationObserver performance? Scoped per component, not global. Use subtree: false when you can, attributeFilter to watch only specific attributes. The browser batches mutations automatically (delivered in microtask), so rapid changes don't mean rapid callbacks.
In practice it's cheaper than React's virtual DOM diffing for most UI. You're not diff-comparing object trees, you're getting notified exactly what changed.
The DATAOS book (https://dataos.software/book) goes deeper on the architecture if you want the full philosophy.