Appearance
Performance
Performance work on Ediky has one theme: the content bank is huge, the client shouldn't feel it. Every technique below is in production; the headline number is the main bundle dropping from ~1,153KB to ~855KB gzipped after the metadata/body split and route lazy-loading landed.
Load less
- Metadata/body split — the core move. List pages fetch a per-section JSON shard; full bodies load on demand; multi-MB hidden tests load only on Submit (Question Bank).
- Lazy routes with preload-on-hover — every route is
React.lazy; hovering a nav link warms its chunk so navigation still feels instant. First paint pays only for the shell + dashboard. - CDN offload for CP — the 600 CP bodies come from jsDelivr, not the Pages deployment.
Fetch once
sessionStorageshard cache (stale-while-revalidate) — reloading a list renders instantly from cache while a background fetch reconciles. Session-scoped on purpose: content updates ship with deploys, so cross-session staleness isn't worth the invalidation complexity.- In-memory LRU of promises for parsed bodies — caching the promise (not the resolved value) means two concurrent requests for one slug share a single in-flight fetch instead of racing. Bounded so an OA that touches 40 questions doesn't grow memory unbounded.
Render less
useWindowedRows— dependency-free row virtualization for the long lists (600 CP rows, 350+ CS questions). Measures container, renders the visible slice + overscan, pads with spacers. Written in-house because the lists are simple fixed-height rows andreact-windowwould've been the largest dependency on the page.useDeferredValueon search inputs — keystrokes update the input at full priority while list filtering re-renders at deferred priority; typing stays smooth over hundreds of rows. This (plus concurrent rendering generally) is the concrete reason the app is on React 19.
Ship less
- Per-language CodeMirror modes load with the editor, not the app; charts (recharts) load with the dashboard; KaTeX/highlight load with content renderers.
_headerssets long-cache for hashed assets and no-cache forindex.html— the fix for stale-SPA deploys where users held an old HTML pointing at purged chunk hashes (Troubleshooting).
Guardrails
ci.ymlruns lint + full build on every push/PR, so a bundle-breaking or lint-failing change can't reachmainquietly.lint-content.mjsruns insidenpm run build, keeping content problems (oversized cases, banned includes, bad slugs) out of the artifact entirely.
What was measured, not assumed
Two decisions here came from measurement contradicting the obvious guess: judge concurrency = 1 beat parallel dispatch on the single-core VM (Code Execution), and virtualization beat memoization for long lists (memoizing rows still paid mount cost for thousands of nodes). Both are the kind of thing worth stating with numbers when asked "what did you optimize?"