Skip to content

Commit 7fbb82c

Browse files
authored
perf(web): optimize thumbnail loading with virtual scrolling and lazy… (#167)
1 parent 3a52858 commit 7fbb82c

File tree

2 files changed

+66
-27
lines changed

2 files changed

+66
-27
lines changed

apps/web/src/components/ui/photo-viewer/GalleryThumbnail.tsx

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,20 @@ export const GalleryThumbnail: FC<{
9191
}
9292
}, [])
9393

94+
// Virtual scrolling optimization: only render thumbnails near the visible area
95+
// Calculate the range of thumbnails to render
96+
const renderRange = 30 // Render 30 items before and after current index, ~60 total
97+
const startIndex = Math.max(0, currentIndex - renderRange)
98+
const endIndex = Math.min(photos.length - 1, currentIndex + renderRange)
99+
100+
// Calculate placeholder widths
101+
const thumbnailWidth = isMobile ? thumbnailSize.mobile : thumbnailSize.desktop
102+
const gapSize = isMobile ? thumbnailGapSize.mobile : thumbnailGapSize.desktop
103+
const itemWidth = thumbnailWidth + gapSize
104+
105+
const leftPlaceholderWidth = startIndex > 0 ? startIndex * itemWidth : 0
106+
const rightPlaceholderWidth = endIndex < photos.length - 1 ? (photos.length - 1 - endIndex) * itemWidth : 0
107+
94108
return (
95109
<m.div
96110
className="pb-safe border-accent/20 bg-material-medium z-10 shrink-0 border-t backdrop-blur-2xl"
@@ -122,33 +136,57 @@ export const GalleryThumbnail: FC<{
122136
padding: isMobile ? thumbnailPaddingSize.mobile : thumbnailPaddingSize.desktop,
123137
}}
124138
>
125-
{photos.map((photo, index) => (
126-
<button
127-
type="button"
128-
key={photo.id}
129-
className={clsxm(
130-
'contain-intrinsic-size relative shrink-0 overflow-hidden rounded-lg border-2 transition-all',
131-
index === currentIndex
132-
? 'scale-110 border-accent shadow-[0_0_20px_color-mix(in_srgb,var(--color-accent)_20%,transparent)]'
133-
: 'grayscale-50 border-accent/20 hover:border-accent hover:grayscale-0',
134-
)}
135-
style={
136-
isMobile
137-
? {
138-
width: thumbnailSize.mobile,
139-
height: thumbnailSize.mobile,
140-
}
141-
: {
142-
width: thumbnailSize.desktop,
143-
height: thumbnailSize.desktop,
144-
}
145-
}
146-
onClick={() => onIndexChange(index)}
147-
>
148-
{photo.thumbHash && <Thumbhash thumbHash={photo.thumbHash} className="size-fill absolute inset-0" />}
149-
<img src={photo.thumbnailUrl} alt={photo.title} className="absolute inset-0 h-full w-full object-cover" />
150-
</button>
151-
))}
139+
{/* Left placeholder */}
140+
{leftPlaceholderWidth > 0 && (
141+
<div
142+
style={{
143+
width: leftPlaceholderWidth,
144+
flexShrink: 0,
145+
}}
146+
/>
147+
)}
148+
149+
{/* Only render thumbnails within visible range */}
150+
{photos.slice(startIndex, endIndex + 1).map((photo, sliceIndex) => {
151+
const index = startIndex + sliceIndex
152+
return (
153+
<button
154+
type="button"
155+
key={photo.id}
156+
className={clsxm(
157+
'contain-intrinsic-size relative shrink-0 overflow-hidden rounded-lg border-2 transition-all',
158+
index === currentIndex
159+
? 'scale-110 border-accent shadow-[0_0_20px_color-mix(in_srgb,var(--color-accent)_20%,transparent)]'
160+
: 'grayscale-50 border-accent/20 hover:border-accent hover:grayscale-0',
161+
)}
162+
style={
163+
isMobile
164+
? {
165+
width: thumbnailSize.mobile,
166+
height: thumbnailSize.mobile,
167+
}
168+
: {
169+
width: thumbnailSize.desktop,
170+
height: thumbnailSize.desktop,
171+
}
172+
}
173+
onClick={() => onIndexChange(index)}
174+
>
175+
{photo.thumbHash && <Thumbhash thumbHash={photo.thumbHash} className="size-fill absolute inset-0" />}
176+
<img src={photo.thumbnailUrl} alt={photo.title} className="absolute inset-0 h-full w-full object-cover" />
177+
</button>
178+
)
179+
})}
180+
181+
{/* Right placeholder */}
182+
{rightPlaceholderWidth > 0 && (
183+
<div
184+
style={{
185+
width: rightPlaceholderWidth,
186+
flexShrink: 0,
187+
}}
188+
/>
189+
)}
152190
</div>
153191
</m.div>
154192
)

apps/web/src/modules/gallery/MasonryPhotoItem.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ export const MasonryPhotoItem = ({ data, width, index: _ }: { data: PhotoManifes
228228
ref={imageRef}
229229
src={data.thumbnailUrl}
230230
alt={data.title}
231+
loading="lazy"
231232
className={clsx('absolute inset-0 h-full w-full object-cover duration-300 group-hover:scale-105')}
232233
onLoad={handleImageLoad}
233234
onError={handleImageError}

0 commit comments

Comments
 (0)