Skip to content

calculator.svelte.ts: back recompute-on-access getters with $derived #689

@grzanka

Description

@grzanka

Part of the code-health / complexity review of src/. Correctness/perf smell
in the hottest state module.

Summary

In src/lib/state/calculator.svelte.ts the public getters that templates read
on every render — rows, validationSummary, and stpDisplayUnit (around
lines 377–397) — each call a compute*() function on every property access
rather than exposing memoized $derived values.

get rows() { return computeRows(); }                 // re-parses every row, every read
get stpDisplayUnit() { return getStpDisplayUnit(); } // re-resolves unit, every read
get validationSummary() { return computeValidationSummary(); }

Why this needs doing

  • computeRows() iterates all input rows and, per row, re-runs parseRow()
    (energy parsing + unit conversion + result lookup) and resolveParticleMass().
    These getters are read multiple times per render (table body, footer,
    validation banner, export wiring), so the same parse work runs several times
    for a single frame.
  • It side-steps Svelte 5's fine-grained reactivity. $derived memoizes and only
    recomputes when a tracked dependency actually changes; a plain getter cannot,
    so we pay full cost on every read regardless of whether inputs changed.
  • It's a latent correctness hazard: any code that reads the getter twice and
    assumes a stable reference (e.g. === identity checks, or capturing then
    comparing) gets a fresh object each time.

The project mandates runes; this is exactly the case $derived exists for.

Scope

  • Introduce module-level $derived / $derived.by values for the three
    computations (and any sibling getter in the file with the same pattern — audit
    the whole returned object).
  • Keep the public getter surface identical (get rows() returns the derived
    value) so no call sites change.
  • Confirm the compute* functions are pure reads of reactive state (no
    side-effects); if any mutate state, that must be untangled first.

Acceptance criteria

  • rows, validationSummary, stpDisplayUnit are backed by $derived.
  • Reading a getter twice without an input change returns the same
    memoized value (add a focused unit test asserting referential stability).
  • No behavioural change in calculator output; existing
    calculator-state.test.ts / calculator-url.test.ts pass unchanged.
  • pnpm check, pnpm lint, pnpm test pass.

Files

  • src/lib/state/calculator.svelte.ts
  • src/tests/unit/calculator-state.test.ts (add memoization assertion)

AI logging

  • CHANGELOG-AI.md entry + short log.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions