# TODO: Performance improvements — Library "All Books" load speed Observed: loading all books (especially on hard refresh CTRL+F5) is slow. Root cause analysis identified four bottlenecks, ordered by impact. --- ### 1. Lazy cover loading (HIGH impact) **Problem:** On every full render, a `` is created immediately for every book card that has a cover. With 500+ books this fires 500+ simultaneous HTTP requests to `/library/cover-cached/`. The browser throttles these and the server gets hammered. **Fix:** Use an `IntersectionObserver` to defer image loading until a card scrolls into the viewport. Cards outside the viewport stay as canvas placeholders until they appear. **Affected:** `static/library.js` — `renderBooksGrid()` second pass (lines ~981–1005) --- ### 2. HTTP caching on `/api/library` (HIGH impact) **Problem:** Every refresh (including soft refresh) fetches the full book list JSON from the server with no caching. No `ETag`, `Last-Modified` or `Cache-Control` headers are set. **Fix:** Add an `ETag` header based on a hash of the serialized response (or a DB row count + last `updated_at`). Browser sends `If-None-Match`; server returns `304 Not Modified` when nothing changed — zero data transfer. **Affected:** `routers/library.py` — `GET /api/library` endpoint; `static/library.js` — `loadLibrary()` --- ### 3. Double DOM pass in renderBooksGrid (MEDIUM impact) **Problem:** `renderBooksGrid` makes two full iterations over the book list: 1. Build all card HTML and append to the grid. 2. Query the DOM again for each `canvas-*` and `wrap-*` element to set up image loading. This causes two reflows and 500+ `getElementById` calls after the DOM is already populated. **Fix:** Combine both passes into one: build the card element, immediately set up the `` element and canvas, then append. No second iteration needed. **Affected:** `static/library.js` — `renderBooksGrid()` (lines ~904–1005) --- ### 4. Tags via separate query + Python merge (LOW impact) **Problem:** `list_library_json()` fetches book tags via a separate `SELECT * FROM book_tags` query and then merges them in Python using a dict. For large libraries this means two round-trips and an O(n) in-process merge. **Fix:** Use a PostgreSQL JSON aggregation in the main query: ```sql COALESCE( json_agg(json_build_object('tag', bt.tag, 'tag_type', bt.tag_type)) FILTER (WHERE bt.tag IS NOT NULL), '[]' ) AS tags ``` This returns tags inline per book row, eliminating the second query and the Python merge loop. **Affected:** `routers/common.py` — `list_library_json()` (lines ~397–458)