perf(cover): stop the loading shimmer once the cover has loaded (#958)

The skeleton kept animate-pulse after the image landed, and opacity-0 could not hide it because a
CSS animation overrides the specified value. Every rendered cover ran an infinite opacity animation
behind an opaque image for the life of the page. Idle library view in Firefox 153: 109% CPU before,
0.0% after.

Closes #626
This commit is contained in:
neon
2026-08-10 11:43:27 -06:00
committed by GitHub
parent 7299664f64
commit bf8bb52fc7
2 changed files with 9 additions and 5 deletions
@@ -144,9 +144,10 @@ function handleImageError() {
<template v-if="canRenderImage">
<span
:class="[
'absolute inset-0 z-0 animate-pulse bg-foreground/5 transition-opacity duration-200 ease-out',
loaded ? 'opacity-0 pointer-events-none' : 'opacity-100',
'absolute inset-0 z-0 bg-foreground/5 transition-opacity duration-200 ease-out',
loaded ? 'opacity-0 pointer-events-none' : 'animate-pulse opacity-100',
]"
data-testid="cover-skeleton"
aria-hidden="true"
/>
<img
@@ -164,8 +164,11 @@ describe('BookCoverArtwork', () => {
await triggerMainImageLoad(wrapper, 600, 900)
expect(wrapper.find('[data-testid="placeholder"]').exists()).toBe(false)
const skeleton = wrapper.find('.animate-pulse')
const skeleton = wrapper.find('[data-testid="cover-skeleton"]')
expect(skeleton.classes()).toContain('opacity-0')
// The shimmer is an infinite animation, so leaving it on a loaded cover keeps the
// refresh driver ticking for the life of the page. See issue #626.
expect(skeleton.classes()).not.toContain('animate-pulse')
})
it('does not render the fitted spine layer when spine is disabled', async () => {
@@ -242,7 +245,7 @@ describe('BookCoverArtwork', () => {
expect(image.classes()).toContain('opacity-100')
expect(image.classes()).not.toContain('transition-opacity')
expect(second.find('.animate-pulse').classes()).toContain('opacity-0')
expect(second.find('[data-testid="cover-skeleton"]').classes()).toContain('opacity-0')
})
it('restores instantly when src changes to an already-loaded cover', async () => {
@@ -255,7 +258,7 @@ describe('BookCoverArtwork', () => {
await wrapper.setProps({ src: '/warm.jpg' })
const image = wrapper.find('img[alt="Dune cover"]')
expect(image.classes()).toContain('opacity-100')
expect(wrapper.find('.animate-pulse').classes()).toContain('opacity-0')
expect(wrapper.find('[data-testid="cover-skeleton"]').classes()).toContain('opacity-0')
})
it('still shows the skeleton when src changes to an unseen cover', async () => {