Appearance
Docs Index | Project README | BetoDashboard v2 | Tradeoffs & Scaling
Engineering Process Behind Our Analysis
This chapter documents how we frame problems, derive requirements, design solutions, and validate outcomes in BetoDashboard. It is a developer-facing record of the engineering reasoning behind our architecture.
Problem Framing in Action
We ground every architectural decision in a concrete pain we observed during development.
Manual DOM Management → Fragility and Reflows
- Problem: Components were mutating
innerHTMLdirectly, duplicating event binding, and causing layout thrash during updates. - Requirement: Safe, minimal-change DOM updates with centralized event wiring.
- Solution: Introduced DOM helpers and event delegation.
TypeScript (actual code):
ts
// packages/core/src/js/dom.ts
export function setHTML(el: Element, html: string) {
(el as HTMLElement).innerHTML = sanitize(html); // DOMPurify
}
export function replaceChildren(el: Element, ...nodes: (Node | string)[]) { /* ... */ }
// packages/core/src/js/events.ts
export function on(root: Document | HTMLElement, type: string, selector: string, handler: (ev: Event, target: Element) => void) { /* ... */ }Adoption example:
ts
// Sidebar now uses setHTML (sanitized)
setHTML(root, `<header class="sidebar-header">...</header>`);
// Delegated events
on(root, 'click', 'button[role=radio]', (_ev, target) => { /* ... */ });
on(root, 'click', '.toast-close', (_ev, btn) => { /* ... */ });💭 Note: Centralized delegation avoids per-node listeners and allowed us to remove multiple addEventListener calls, reducing listener count and GC pressure.
Boilerplate for Complex UIs → Repeated Logic
- Problem: Repeated patterns for derived values, async status, and single-key store access.
- Requirement: Small, composable primitives to compose behavior without framework lock-in.
- Solution:
createDerived,createAsyncAction, andcreateSliceutilities.
TypeScript (actual code):
ts
// packages/core/src/js/derived.ts
export function createDerived(keys, compute, cb?) { /* computes from store keys */ }
// packages/core/src/js/asyncAction.ts
export function createAsyncAction(fn) { /* status: idle/loading/success/error */ }
// packages/core/src/js/slice.ts
export function createSlice(key) { /* get/set/on for a single store key */ }Usage examples:
ts
const themeDir = createDerived(['theme', 'dir'], (s) => `${s.theme}:${s.dir}`);
themeDir.subscribe(v => document.documentElement.dataset.themeDir = v);
const sidebar = createSlice('sidebar');
sidebar.set('collapsed');
const loadUsers = createAsyncAction(async () => fetch('/api/users').then(r => r.json()));
await loadUsers.run();💭 Note: Converting repeated patterns to primitives lowered component LOC and made status handling consistent across modules.
Learning Curve → Inconsistent Patterns
- Problem: Custom ad-hoc patterns made it harder for new engineers to navigate the code.
- Requirement: Familiar structures (delegation, minimal base class, explicit store APIs) with strong typing.
- Solution:
BaseComponentwith scoped delegation and effects; explicit exports from@betodashboard/core.
TypeScript (actual code):
ts
// packages/core/src/components/BaseComponent.ts
export class BaseComponent {
protected on(type: any, selector: string, handler: (ev: any, target: Element) => void) { /* ... */ }
protected effect(setup: () => (() => void) | void) { /* lifecycle-registered disposer */ }
}💭 Note: The base class is intentionally small. It standardizes lifecycle without forcing a framework-level component model.
Requirements Analysis Process
We translate problems into requirements, then pick verifiable implementations.
Safety
- Implementation:
setHTMLsanitizes with DOMPurify; CSP enforced;sanitize.tsused in all HTML paths. - Verification: Unit tests for sanitization; Playwright a11y scans (axe-core); CI
npm audit --productiongate. - Why it scales: Reduces security regressions as team size grows.
- Implementation:
Performance
- Implementation: Delegated events; minimal HTML updates; planned fragment-diff step; Lighthouse budgets; bundle chunking for vendor.
- Verification: Lighthouse CI with budgets; bundle gate via
scripts/check-bundle.mjs; planned reflow sampling via PerformanceObserver. - Why it scales: Predictable budgets enable component growth without hidden costs.
Maintainability
- Implementation: Slices/Derived/AsyncAction; BaseComponent; clear separation in
packages/corevs demo app. - Verification: LOC reduction per component; test readability; churn tracking in git.
- Why it scales: Reusable primitives reduce cognitive overhead and duplication.
- Implementation: Slices/Derived/AsyncAction; BaseComponent; clear separation in
Composition
- Implementation: DCE (
define,mountAll), data-props JSON, slots via<template data-slot>. - Verification: E2E mounts (playwright) and story-driven examples; integration tests.
- Why it scales: Decouples authoring from integration (works across MPA/SPAs).
- Implementation: DCE (
Debugging & Observability
- Implementation: Consistent store APIs, error-boundary, Playwright traces;
web-vitalsreporting. - Verification: E2E traces; screenshots; perf logs during runs.
- Why it scales: Production issues are quicker to triage with consistent patterns.
- Implementation: Consistent store APIs, error-boundary, Playwright traces;
Solving the Right Problem — Validation Cases
Short case studies from our dev cycle.
- Replace direct DOM mutations with setHTML/replaceChildren
- Scenario: Components were reconstructing inner markup and rebinding listeners on every change.
- Observation: After migrating to
setHTML+ delegated events, the number of active event listeners in devtools dropped; visual jank decreased when toggling Sidebar/Modal. - Result: Fewer reflows during interactive updates; simpler cleanup.
💭 Note: In local profiling, layout-related time during Sidebar toggle fell noticeably; we plan to codify this with PerformanceObserver sampling.
- Event delegation for toasts and theme buttons
- Scenario: Toast close buttons and theme radios added and removed frequently.
- Observation:
on(root, 'click', selector, ...)eliminated repeated binds per element instance. - Result: Lower memory churn and fewer leaked handlers when rapidly adding/removing toasts.
- StateSlice + Derived reduce duplication
- Scenario: Repeated access patterns for single keys and composites (theme+dir).
- Observation: Replaced boilerplate with
createSlice('sidebar')andcreateDerived(...). - Result: Cleaner components; easier unit testing; less code churn when store shape evolves.
Technical Validation Framework
How we evaluate whether a solution is working — and when to iterate.
Planned measurements and harnesses:
DOM Performance
- Render cycles: count updates per interaction (e.g., Sidebar toggle, Modal open/close).
- Fragment-diff time: micro-bench
replaceChildrenvs naive innerHTML for typical nodes. - Reflow frequency: PerformanceObserver + DevTools performance profiling in CI (headless samples).
State Consistency
- Mutation tracking: instrument store.set keys and update latency (ms) per dispatch.
- Transaction validation: batch updates (future) to ensure atomic UI transitions.
Developer Experience
- Onboarding: track time from clone → first green build.
- Cognitive load: LOC per component before/after primitives; count of event bindings per component.
- Error discoverability: time-to-fix for common failures (scripts to surface diffs, failing gates).
Tooling pipeline:
- Lighthouse budgets (lighthouserc.json + lighthouse-budgets.json)
- Bundle gate (scripts/check-bundle.mjs)
- Playwright + axe (accessibility.spec.ts)
- CI coverage gates (vitest coverage thresholds)
- Optional perf dashboards (future): serialize PerformanceObserver samples; visualize trends per PR.
Architectural Connections
This process directly informs long-term architecture:
Virtual Fragment Rendering
- From “minimize DOM churn” → fragment-based updates and (planned) virtual fragment diffing for predictable reflow control.
Plugin System
- From “extensibility and autonomy” → a formal component/slot contract with isolated lifecycles and clear store boundaries.
Config-Driven Modules
- From “consistency at scale” →
beto.config(planned) driving tokens, CSP, and guardrails across apps.
- From “consistency at scale” →
These are not isolated features; they emerge from disciplined problem framing and verifiable requirements.
Developer Commentary (inline examples)
ts
// Modal focus trap (excerpt) using BaseComponent effects
const keyListener = (e: KeyboardEvent) => this.trapTab(e);
document.addEventListener('keydown', keyListener as any);
this.effect(() => () => document.removeEventListener('keydown', keyListener as any));💭 Note: Using effect ensures cleanup is tied to component lifetime — no orphan handlers after unmount.
ts
// Delegated radio group handling
this.on('click' as any, 'button[role=radio]', (_ev, target) => {
const next = (target as HTMLButtonElement).dataset.theme as State['theme'];
if (next === 'light' || next === 'dark' || next === 'auto') store.set('theme', next);
});💭 Note: Delegation lets the markup renew without rebinding listeners.
ts
// Derived state powering document attributes
const themeDir = createDerived(['theme', 'dir'], (s) => `${s.theme}:${s.dir}`);
themeDir.subscribe(v => document.documentElement.dataset.themeDir = v);💭 Note: Keeping derivations out of components prevents subtle duplication and makes testing trivial.
Engineering Lessons Learned
Problem-first discipline works
- Starting with the real pain (DOM fragility, boilerplate, learning curve) led to small, high-leverage primitives instead of over-engineering.
Developer experience drives good architecture
- Reducing boilerplate (Slices/Derived/AsyncAction) improved readability and testability, which in turn made the system safer to evolve.
Always define measurable requirements before coding
- Budgets, gates, and test signals make decisions auditable; they’re the guardrails for team growth.
Lifecycle-aware patterns prevent leaks
- Effects and delegation ensure no orphaned listeners and predictable cleanup.
Prepare for scale early, but evolve incrementally
- Virtual fragment rendering, plugin APIs, and config-driven modules are natural next steps — grounded in the problems we validated.
Next: See the Tradeoffs & Scaling Strategy for long-term architectural paths and the Execution Roadmap v2 for phase gates and KPIs.
Also see: Engineering Metrics & Validation for how we collect and track measurable signals.
See: Development Morphology for standard forms and scaffolding rules that make features buildable, repeatable, and extensible.