Skip to content

reader: fix spinner stuck forever on slow first page-image load#711

Merged
ajslater merged 1 commit intov1.11-performancefrom
claude/reader-spinner-keep-img-mounted
May 4, 2026
Merged

reader: fix spinner stuck forever on slow first page-image load#711
ajslater merged 1 commit intov1.11-performancefrom
claude/reader-spinner-keep-img-mounted

Conversation

@ajslater
Copy link
Copy Markdown
Owner

@ajslater ajslater commented May 4, 2026

Summary

The reader page wrapper used a v-else-if / v-else chain that made LoadingPage and the image component mutually exclusive. When the 333ms mounted() timer flipped showProgress=true, Vue swapped to LoadingPagedestroying the <img> element along with its @load listener. Any response arriving after that landed on a torn-down element, loaded never flipped, and the spinner stuck forever.

The fix renders <component> and LoadingPage as siblings under v-else with v-show toggling component visibility instead of v-if. The component stays mounted across the spinner transition, the @load handler is still wired when the response lands, and the visible behaviour matches the previous template at each state.

Why it only repro'd in production

The bug needs the page-image request to cross 333ms.

  • Local make dev-server / make dev-prod-server → API on localhost, request returns well under 333ms → swap never fires → no repro.
  • Production (Docker behind nginx/swag), cold caches → first request to /api/v3/c/<pk>/<page>/page.jpg crosses 333ms → swap fires → <img> unmounts mid-flight.

The reproducer's other quirks fall out cleanly:

  • Force refresh fixes it → image is now in the browser disk cache → next attempt loads in <333ms.
  • Closing & reopening the same book fixes it → same browser cache hit.

Confirmed via DOM inspection on a stuck instance

> document.querySelectorAll('#booksWindow .img').length
1
> document.querySelectorAll('#booksWindow .pageLoading').length
2
> document.querySelector('#booksWindow .page')?.innerHTML?.slice(0, 200)
'<div ... class="pageLoading">…</div>'

The current page's <img> is gone; only the spinner remains.

Behavior matrix (unchanged externally)

state image visible? spinner visible?
0 – 333 ms (loading) yes no
> 333 ms still loading hidden yes
onLoad → loaded=true yes unmounted
error unmounted unmounted (ErrorPage shown)

Test plan

  • bun run build (frontend) clean
  • bun run test:ci (vitest) passes
  • eslint + prettier clean for the changed file
  • Verify against the production Docker build: open a book that previously stuck, watch the spinner appear briefly, then the page render once the image lands.
  • Sanity: open a book on a fast localhost (make dev-prod-server), confirm no visible spinner flash on cached-image second open, normal spinner on first slow open.

🤖 Generated with Claude Code

The ``v-else-if`` / ``v-else`` chain on the page wrapper made
``LoadingPage`` and the image component mutually exclusive. When
the 333ms ``mounted()`` timer flipped ``showProgress=true``, Vue
swapped to ``LoadingPage`` — destroying the ``<img>`` element along
with its ``@load`` listener. Any response arriving after that
landed on a torn-down element, ``loaded`` never flipped, and the
spinner stuck forever.

Only reproduced in production: behind an nginx reverse proxy with
cold caches, the first page-image request crossed 333ms; on
``make dev-prod-server`` against ``localhost`` the image came back
well under the threshold so the swap never fired.

Render ``<component>`` and ``LoadingPage`` as siblings under
``v-else`` with ``v-show`` toggling component visibility instead
of ``v-if``. The component stays mounted across the spinner
transition, the ``@load`` handler is still wired when the response
lands, and the visible behaviour matches the previous template at
each state (image during 0–333ms grace, spinner while loading
beyond that, image once loaded, ErrorPage on error).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ajslater ajslater merged commit 58e4e1a into v1.11-performance May 4, 2026
1 check failed
@ajslater ajslater deleted the claude/reader-spinner-keep-img-mounted branch May 4, 2026 02:46
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.

1 participant