novela/docs/TODO-PERF-library-load.md
Ivo Oskamp 00e75a6106 Add duplicate detection, Convert warning, and performance TODO
- 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>
2026-03-27 16:22:02 +01:00

2.6 KiB
Raw Blame History

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.jsrenderBooksGrid() second pass (lines ~9811005)


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.pyGET /api/library endpoint; static/library.jsloadLibrary()


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.jsrenderBooksGrid() (lines ~9041005)


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:

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.pylist_library_json() (lines ~397458)