perf(frontend): round 6 — coalesced progressive listing, in-place SvelteSet, batch fan-out, t() value cache

Four SPA hot-path fixes, each shipping with a vitest benchmark gate
(verbatim BEFORE replica + equivalence + perf assertion) so CI
re-verifies the win on every run:

- fetchFolderListing invoked onPage after EVERY 200-row page with the
  whole accumulated listing, and the files view re-sorts everything per
  emission — O(N²/page) main-thread work on large folders. Page one and
  the final page always emit; intermediates coalesce to one per 150 ms.
  25×200 load: 30.9 → 4.0 ms (7.8x), 65 000 → 5 200 sorted elements.

- selected/favoriteIds/sharedIds (files) and favoriteIds (recent) were
  $state<Set>s copied whole on every toggle. Now one SvelteSet each,
  mutated in place (the useSelection pattern): 1 000 toggles @ N=5 000
  771.9 → 1.9 ms (399x); one-toggle fan-out across 40 mounted rows
  40 → 3 re-runs when refining a select-all.

- batchDelete/moveInto awaited one request per item serially and
  probed listing.folders.find per id (O(N·M)). Now an id index built
  once + mapLimit(6) fan-out, failure semantics preserved: 100-item
  delete @ 5 ms RTT 525 → 89 ms (5.9x), 38 825 → 500 probes.

- t() re-split its dotted key and walked the nested dict on every call,
  and interpolate regex-scanned strings without placeholders. Resolved
  values now memoize per (dict, key) in a WeakMap + a {{ guard:
  20k mixed calls 22.7 → 8.6 ms (2.63x).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017aJu9ghvuT8WqC31ZEGTBA
This commit is contained in:
Claude
2026-07-18 00:54:38 +00:00
parent 63cf6646d0
commit 61c9470981
10 changed files with 905 additions and 71 deletions
+167
View File
@@ -0,0 +1,167 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { getNestedValue, interpolate } from './index.svelte';
/**
* Benchmark gate for the `t()` hot path: the split-path cache in
* `getNestedValue` and the `{{` guard in `interpolate`.
*
* Audit finding: the locale dicts are nested, so every `t('a.b.c')` call
* re-split its key into a fresh array and walked the tree, and `interpolate`
* ran its global-regex `.replace` scan even though the vast majority of UI
* strings carry no `{{placeholder}}`. A rendered list row calls `t()` ~10×,
* so a 40-row paint pays ~400 walk+split-allocs + regex scans. The fix
* caches the resolved value per (dict, key) — dicts are load-once-immutable
* and the key set is the app's finite static strings — and skips the regex
* when the string has no `{{`.
*
* Gates: byte-identical results vs the pre-fix reference implementations
* across the real shipped en.json (nested keys, flat keys, underscore
* fallback, missing keys, placeholder strings — cold AND warm, so a stale or
* poisoned cache entry fails loudly), and a ≥1.5x speedup on a mixed
* 20k-call workload.
*/
type Dict = { [key: string]: string | Dict };
const enDict = JSON.parse(
readFileSync(resolve(__dirname, '../../../static/locales/en.json'), 'utf8')
) as Dict;
/** Pre-fix `getNestedValue`, verbatim: fresh `split('.')` on every call. */
function referenceGetNestedValue(obj: Dict | undefined, path: string): string | null {
if (obj && typeof obj === 'object' && path in obj) {
const value = obj[path];
return typeof value === 'string' ? value : null;
}
const keys = path.split('.');
let current: unknown = obj;
for (const key of keys) {
if (current && typeof current === 'object' && key in (current as Dict)) {
current = (current as Dict)[key];
} else {
if (path.includes('_') && !path.includes('.')) {
const [prefix, ...parts] = path.split('_');
const suffix = parts.join('_');
const branch = obj?.[prefix];
if (branch && typeof branch === 'object' && suffix in (branch as Dict)) {
const v = (branch as Dict)[suffix];
return typeof v === 'string' ? v : null;
}
}
return null;
}
}
return typeof current === 'string' ? current : null;
}
/** Pre-fix `interpolate`, verbatim: unconditional regex `.replace`. */
function referenceInterpolate(text: string, params: Record<string, unknown>): string {
return text.replace(/{{\s*([^}]+)\s*}}/g, (_, key: string) => {
const k = key.trim();
return params[k] !== undefined ? String(params[k]) : `{{${key}}}`;
});
}
/** Every dotted leaf path in the dict (the app's real key population). */
function collectKeys(obj: Dict, prefix = '', out: string[] = []): string[] {
for (const [k, v] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${k}` : k;
if (typeof v === 'string') out.push(path);
else collectKeys(v, path, out);
}
return out;
}
const allKeys = collectKeys(enDict);
// A workload mix mirroring real renders: mostly present nested keys, plus
// underscore-fallback forms, flat keys, and misses.
const workload: string[] = [
...allKeys,
'errors_loadFailed', // underscore fallback form
'groupby_modifiedAt',
'nav.files',
'this.key.does.not.exist',
'nokey',
'files.deeply.missing.leaf'
];
const PARAMS = { n: 42, count: 7, email: 'x@y.z', name: 'Ada' };
describe('t() hot path: split cache + interpolate guard (benchmark gate)', () => {
it('getNestedValue is byte-identical to the split-per-call reference on every real key', () => {
expect(allKeys.length).toBeGreaterThan(300);
for (const key of workload) {
expect(getNestedValue(enDict, key), key).toBe(referenceGetNestedValue(enDict, key));
}
// Repeat with the cache warm — a poisoned/shared split array would show here.
for (const key of workload) {
expect(getNestedValue(enDict, key), `warm:${key}`).toBe(referenceGetNestedValue(enDict, key));
}
});
it('interpolate is byte-identical to the unguarded reference', () => {
const texts = [
// Keys whose segments contain literal dots aren't resolvable via a
// dotted path — drop the nulls (both implementations agree on them,
// covered by the lookup-equivalence test above).
...allKeys
.map((k) => referenceGetNestedValue(enDict, k))
.filter((v): v is string => v !== null),
'Move {{n}} items to trash?',
'{{ n }} spaced', // padded placeholder
'{{unknown}} stays intact',
'no placeholders at all',
'brace but not double { x }',
'{{n}}{{count}}back-to-back',
''
];
let withPlaceholders = 0;
for (const text of texts) {
if (text.includes('{{')) withPlaceholders++;
expect(interpolate(text, PARAMS), JSON.stringify(text)).toBe(
referenceInterpolate(text, PARAMS)
);
expect(interpolate(text, {}), `noparams:${JSON.stringify(text)}`).toBe(
referenceInterpolate(text, {})
);
}
// The workload genuinely exercises both branches of the guard.
expect(withPlaceholders).toBeGreaterThan(50);
expect(withPlaceholders).toBeLessThan(texts.length / 2);
});
it('20k mixed lookups+interpolations run ≥1.5x faster (perf gate)', { timeout: 30_000 }, () => {
const N = 20_000;
// The t() body for a hit: nested lookup then interpolate the result.
const after = (key: string): string => {
const v = getNestedValue(enDict, key);
return v === null ? key : interpolate(v, PARAMS);
};
const before = (key: string): string => {
const v = referenceGetNestedValue(enDict, key);
return v === null ? key : referenceInterpolate(v, PARAMS);
};
let sink = 0;
for (let i = 0; i < 2_000; i++) {
sink += after(workload[i % workload.length]).length;
sink += before(workload[i % workload.length]).length;
}
const t0 = performance.now();
for (let i = 0; i < N; i++) sink += after(workload[i % workload.length]).length;
const afterMs = performance.now() - t0;
const t1 = performance.now();
for (let i = 0; i < N; i++) sink += before(workload[i % workload.length]).length;
const beforeMs = performance.now() - t1;
expect(sink).toBeGreaterThan(0);
console.info(
`t() hot path x ${N}: cached+guarded ${afterMs.toFixed(1)} ms vs split+regex-per-call ${beforeMs.toFixed(1)} ms (${(beforeMs / afterMs).toFixed(2)}x)`
);
expect(afterMs).toBeLessThan(beforeMs / 1.5);
});
});
+28
View File
@@ -116,8 +116,33 @@ export function resolveBrowserLocale(
return 'en';
}
// Resolved-value cache, one map per dict object: `t()` runs ~10× per rendered
// list row over the app's finite static key set, so the nested split + tree
// walk runs once per (locale, key) instead of on every call. Dicts are
// assigned once in `loadDict` and never mutated, so entries can't go stale;
// the cap only guards against a pathological dynamic-key caller.
const RESOLVED_CACHE_MAX = 4000;
const resolvedCache = new WeakMap<Dict, Map<string, string | null>>();
/** Resolve a dot-notation key with a prefix_suffix underscore fallback. */
export function getNestedValue(obj: Dict | undefined, path: string): string | null {
if (!obj || typeof obj !== 'object') return resolveNestedValue(obj, path);
let cache = resolvedCache.get(obj);
if (cache === undefined) {
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- deliberately non-reactive: a memo written during render must not create/notify signals
cache = new Map();
resolvedCache.set(obj, cache);
}
const hit = cache.get(path);
if (hit !== undefined) return hit;
const value = resolveNestedValue(obj, path);
if (cache.size >= RESOLVED_CACHE_MAX) cache.clear();
cache.set(path, value);
return value;
}
/** The uncached lookup: flat-key fast path, dotted walk, underscore fallback. */
function resolveNestedValue(obj: Dict | undefined, path: string): string | null {
if (obj && typeof obj === 'object' && path in obj) {
const value = obj[path];
return typeof value === 'string' ? value : null;
@@ -146,6 +171,9 @@ export function getNestedValue(obj: Dict | undefined, path: string): string | nu
/** Replace `{{param}}` placeholders; leaves unknown placeholders intact. */
export function interpolate(text: string, params: Record<string, unknown>): string {
// The vast majority of UI strings carry no placeholder — skip the regex
// scan (and its per-call machinery) for them.
if (!text.includes('{{')) return text;
return text.replace(/{{\s*([^}]+)\s*}}/g, (_, key: string) => {
const k = key.trim();
return params[k] !== undefined ? String(params[k]) : `{{${key}}}`;