- Convert: warn when title+author already exists in library (preload check) - Library: Duplicates sidebar section with grouped view and live counter - Fix: Duplicates view cover loading now uses same canvas/two-pass pattern as renderBooksGrid - Docs: add TODO-PERF-library-load.md with four identified bottlenecks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
53 lines
2.6 KiB
Markdown
53 lines
2.6 KiB
Markdown
# 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 `<img>` 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 `<img>` 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)
|