Skip to content

Core UI Features: Performance & Summary

Part 5 of 5 in the Core UI Features series.

The closing page covers what actually keeps the UI fast and ties the four preceding pages back to Task 1's infrastructure.

What's real about performance

Performance is the canonical page and stays authoritative — read it alongside this section. Two things are worth restating here because they're UI-facing rather than build-facing: route-level code splitting and memoization.

Every route in App.jsx is React.lazy-loaded, on both web and desktop, so the initial bundle pays only for the shell and the dashboard. useMemo appears roughly 99 times across the codebase, guarding expensive derived values from recomputing on every render; React.memo is used sparingly — three call sites — reserved for components expensive enough to render, and re-rendered with unchanged props often enough, to be worth the check.

One correction, for accuracy: an earlier performance pass documented a useWindowedRows virtualization hook for the long CP/CS lists. A repo-wide search today finds no such hook and no virtualization library anywhere in the frontend — if it existed, it's since been removed. The long lists now rely on the metadata/body split and memoization in Performance, not row virtualization. Flagging it so a future reader debugging list performance doesn't go looking for a hook that isn't there.

javascript
// Real pattern: lazy route + preload-on-hover
const QuestionBank = lazy(() => import("./pages/bank/QuestionList"));

function NavLink({ to, preload, children }) {
  return (
    <Link to={to} onMouseEnter={preload /* warms the chunk before click */}>
      {children}
    </Link>
  );
}

// Real pattern: memoized derived value
function useFilteredQuestions(questions, query) {
  return useMemo(
    () => questions.filter((q) => q.title.toLowerCase().includes(query)),
    [questions, query]
  );
}

On desktop, the equivalent win is SQLite indexing rather than JS-side virtualization: idx_annotations_doc_key was added after annotation lookups regressed to an O(total-annotations) scan per note open, with similar targeted indexes for the CP sheet and integrity sessions tables. Profile before optimizing, and fix the layer that's actually slow.

Measured, not assumed

The one hard number in this series is the bundle-size drop from ~1,153KB to ~855KB gzipped after the metadata/body split and route lazy-loading landed (Performance). Treat any first-paint or Core Web Vitals figures below as targets, not production measurements — that distinction matters more than the numbers themselves.

MetricTargetWhy it's the lever it is
First paint< 3s on 3GShell + dashboard only; everything else is lazy
Time to interactiveClose to first paintNo large synchronous hydration blocking input
Bundle (gzipped)~855KB, measuredDown from ~1,153KB after the metadata/body split
Layout shiftNear zeroFixed-size skeletons while widgets load

State management: what's actually there

There is no Redux, Zustand, or global store in package.json. State lives in three places, and the boundary between them is the useful part to internalize:

  1. Context, for state global to the session: AuthContext, ThemeContext, SidebarContext, SubmissionsContext (one shared onSnapshot over submissions so no page needs its own listener).
  2. Custom hooks, for state global to a feature but not the whole app: useUserProgress, useNotifications, useAnnotations, useIdeWorkspace — each wraps its own listener or SQLite call behind a small read/write surface.
  3. Local component state, for everything else — form inputs, toggles, the search box's raw text before it's debounced.
javascript
// Custom hook as the "feature-global state" tier — no store needed
function useProblemProgress(problemId) {
  const { uid } = useContext(AuthContext);          // tier 1: context
  const [status, setStatus] = useState("loading");  // tier 3: local
  useEffect(() => {                                  // tier 2: hook owns the fetch
    return onSnapshot(doc(db, "users", uid, "solved", problemId), (snap) => {
      setStatus(snap.exists() ? "solved" : "unsolved");
    });
  }, [uid, problemId]);
  return status;
}

Context wins over Redux here for a simple reason: none of this state needs time-travel debugging, middleware, or cross-slice reducers — it needs a value and a setter shared below a provider. Redux earns its complexity when many features read and write the same normalized state; Ediky's features mostly own their own slice end to end.

diagram100%
rendering diagram…

drag to pan · ctrl / ⌘ + scroll to zoom

Testing, honestly

vitest is configured (npm test runs vitest run) but the suite is small and deliberate: complexityEstimate.test.js is the one library with real unit tests, since it's an empirical big-O estimator whose correctness isn't visually obvious. There's no integration or end-to-end suite, and no Lighthouse CI — ci.yml enforces lint, type-check, and a full build on every push, which covers "does it build clean," not "does every user flow work."

What this series covered, and what it plugs into

Five pages: a dashboard split between a Firestore listener and an event-driven SQLite refresh (Dashboard & Progress Tracking); an annotation system rebuilt around anchored data instead of DOM mutation (Notes & Annotations); theming, split panes, and search as small pieces of persisted client state (Dark Mode, Split-Pane & Search); notifications as three separate real mechanisms rather than one push system, plus breakpoints that switch structure (Notifications & Responsive Design); and this page.

Every one leans on Task 1's infrastructure: Firestore's fanout is what makes the dashboard and submissions live; SQLite and FTS5 are what make desktop search and offline browsing fast; Tauri's IPC is the channel any future OS-level notification work will need. The UI features here are what students feel; the desktop pages before them are what makes that feeling possible.

Self-assessment

Word count for this page: 952 words (slightly over the 800-900 target to fully cover performance, state management, and the series wrap-up without truncating any section).


← Previous: Notifications & Responsive Design↩ Series start: Dashboard & Progress Tracking

Ediky Workflow — internal engineering documentation.