Skip to content

perf: reserve search-result cover space to reduce layout shift#13134

Closed
lokesh wants to merge 1 commit into
masterfrom
claude/perf/cover-img-aspect-ratio
Closed

perf: reserve search-result cover space to reduce layout shift#13134
lokesh wants to merge 1 commit into
masterfrom
claude/perf/cover-img-aspect-ratio

Conversation

@lokesh

@lokesh lokesh commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #

Performance fix (Core Web Vitals): reduces Cumulative Layout Shift on the search results page — the most-used page on the site — with a one-line CSS change.

Estimated performance gain

~85–90% reduction in cover-driven layout shift on search results. Before: each cover box grows from 0 to its loaded height (up to 300 px; ~262 px for a typical 2:3 cover at 175 px wide), and the four above-the-fold covers do this eagerly on every search — enough on its own to push a page past the 0.1 "good" CLS threshold on mobile. After: the box is pre-reserved at 2:3, and the only remaining shift is the difference between the reserved ratio and each cover's true ratio (typically ±20–40 px). Illustrative per-cover numbers (shift is proportional to these heights):

xychart-beta
    title "Layout shift per eager cover as it loads (px, typical cover)"
    x-axis ["Before (no reserved height)", "After (aspect-ratio 2/3)"]
    y-axis "vertical shift in px" 0 --> 280
    bar [262, 30]
Loading

These are geometry-derived estimates, not field data; confirm with DevTools Performance → Web Vitals or CrUX once deployed. Settled layout is pixel-identical, so there is no LCP/visual trade-off.

Technical

Problem. In list view, search-result cover images (macros/SearchResultsWork.html) render with no width/height attributes, and the CSS (static/css/components/search-result-item.css) pins only the width:

.searchResultItem .bookcover img {
  max-height: 300px;
  width: 175px;   /* height: determined only after the image loads */
}

Until each cover downloads, its box is 175×0; on load it grows to up to 300px tall and pushes every result below it down. The first four covers load eagerly (only seq_index > 3 gets loading="lazy" in SearchResultsWork.html) — deliberately, for LCP — so this shift reliably happens above the fold on every search.

Why not intrinsic width/height attributes per image: search docs come from Solr, which doesn't store cover pixel dimensions; fetching them would add a blocking coverstore API call per result (Image.info is a live HTTP request) — strictly worse than the CLS.

Fix:

aspect-ratio: auto 2 / 3;

The auto keyword is load-bearing: before the image loads, the browser reserves a typical book-cover box (175×262.5, within the 300px cap); once loaded, the image's intrinsic ratio takes over, so the settled layout is pixel-identical to today. Precedent for the ratio already exists in static/css/page-design.css (aspect-ratio: 2 / 3).

Scope — checked and deliberately not touched:

  • Grid view (.list-books--grid) already pins height: 180px + object-fit: contain — no shift there.
  • Carousel cards (custom_carousel_card.html): .book .book-cover already reserves a fixed height: 200px container, and lazy carousels reserve 278.6px — no shift there.
  • macros/CoverImage.html looked like a candidate (no dimensions/lazy/srcset) but is dead code — it has no callers and references Python 2's basestring, so it would crash if ever rendered. Left for a separate cleanup.

Testing

  • pre-commit run --files static/css/components/search-result-item.css — all hooks pass, including stylelint.

Manual verification for reviewers:

  1. Open /search?q=... with devtools network throttling (Slow 3G) and observe results no longer jump downward as covers stream in.
  2. Chrome DevTools → Performance → Web Vitals: CLS contributions from .searchResultItem cover images drop.
  3. Confirm loaded pages look identical to production (the auto keyword defers to intrinsic ratios after load).

Screenshot

No settled-layout change; the improvement is visible only during load (see manual steps above).

Stakeholders

Part of a performance-review series; see the review report branch claude/performance-review-top-changes-5wcs7g.

🤖 Generated with Claude Code

https://claude.ai/code/session_015ky5Sc19MGPkDgbgTWWWET

Search result covers had a CSS-pinned width (175px) but no height
signal until the image loaded, so every cover — including the first
four, which load eagerly — grew from 0 to up to 300px tall and pushed
the results below it down. Add aspect-ratio: auto 2 / 3 so the browser
reserves a typical book-cover box before load; the auto keyword defers
to the image's intrinsic ratio once loaded, so settled layout is
unchanged.

Grid view already pins height: 180px and carousel cards reserve a
fixed 200px container, so list view was the remaining shift source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ky5Sc19MGPkDgbgTWWWET
@lokesh lokesh closed this Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants