perf: memoize PhotoGrid assets map to prevent re-renders during lightbox navigation [#5]
Extracted the grid mapping logic out of the return block into a `useMemo` hook depending on `[assets, layout, openLightbox]`. This prevents the entire photo grid—including complex Next.js `<Image>` and `<FadeIn>` components—from needlessly re-rendering every time the user navigates via the lightbox arrows or presses next/prev buttons, which previously caused `lightboxIndex` to update and trigger a re-render of the entire grid. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
4
.jules/bolt.md
Normal file
4
.jules/bolt.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
## YYYY-MM-DD - [Memoize grid item re-rendering]
|
||||||
|
|
||||||
|
**Learning:** `useMemo` can significantly boost the performance of long list or grid components (like `PhotoGrid` component) that otherwise gets re-rendered on simple interactions like navigating the image lightbox. Running standard formatting `pnpm format` applies huge changes across entire directories, so it's generally best to format and clean up ONLY modified files to avoid huge diff pollution in code review.
|
||||||
|
**Action:** Be extremely cautious of running format/lint commands at the root codebase; specify only modified directories or files, or avoid formatting if it cascades globally. Focus on targeted memoization where state changes dictate expensive array mapping operations.
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Lightbox } from '@/components/Lightbox';
|
import { Lightbox } from '@/components/Lightbox';
|
||||||
import { FadeIn } from '@/components/FadeIn';
|
import { FadeIn } from '@/components/FadeIn';
|
||||||
@@ -136,50 +136,56 @@ export function PhotoGrid({ assets, layout = 'masonry', gridStyle }: PhotoGridPr
|
|||||||
};
|
};
|
||||||
}, [lightboxIndex, closeLightbox, goNext, goPrev]);
|
}, [lightboxIndex, closeLightbox, goNext, goPrev]);
|
||||||
|
|
||||||
|
// Memoize grid items to prevent re-renders of the entire grid (which includes
|
||||||
|
// Image and FadeIn components) when navigating the lightbox.
|
||||||
|
const gridItems = useMemo(() => {
|
||||||
|
return assets.map((asset, index) => (
|
||||||
|
<FadeIn key={asset.id} delay={index < 12 ? index * 50 : 0}>
|
||||||
|
<div
|
||||||
|
className={`photo-grid__item${
|
||||||
|
layout === 'showcase' && index === 0 ? ' photo-grid__featured' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => openLightbox(index)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
openLightbox(index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`View photo ${index + 1}`}
|
||||||
|
style={{
|
||||||
|
...(asset.dominantColor ? { backgroundColor: asset.dominantColor } : {}),
|
||||||
|
...((layout === 'masonry' || layout === 'showcase') && asset.aspectRatio
|
||||||
|
? { aspectRatio: `${asset.aspectRatio}` }
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={asset.thumbUrl}
|
||||||
|
alt=""
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 600px) 50vw, (max-width: 1000px) 33vw, 25vw"
|
||||||
|
loading={index < 6 ? 'eager' : 'lazy'}
|
||||||
|
{...(asset.blurDataURL
|
||||||
|
? { placeholder: 'blur' as const, blurDataURL: asset.blurDataURL }
|
||||||
|
: {})}
|
||||||
|
/>
|
||||||
|
{(asset.camera || asset.lens) && (
|
||||||
|
<div className="photo-grid__item-exif">
|
||||||
|
{[asset.camera, asset.lens, asset.focalLength].filter(Boolean).join(' · ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FadeIn>
|
||||||
|
));
|
||||||
|
}, [assets, layout, openLightbox]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`photo-grid photo-grid--${layout}`} style={gridStyle}>
|
<div className={`photo-grid photo-grid--${layout}`} style={gridStyle}>
|
||||||
{assets.map((asset, index) => (
|
{gridItems}
|
||||||
<FadeIn key={asset.id} delay={index < 12 ? index * 50 : 0}>
|
|
||||||
<div
|
|
||||||
className={`photo-grid__item${
|
|
||||||
layout === 'showcase' && index === 0 ? ' photo-grid__featured' : ''
|
|
||||||
}`}
|
|
||||||
onClick={() => openLightbox(index)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
openLightbox(index);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={`View photo ${index + 1}`}
|
|
||||||
style={{
|
|
||||||
...(asset.dominantColor ? { backgroundColor: asset.dominantColor } : {}),
|
|
||||||
...((layout === 'masonry' || layout === 'showcase') && asset.aspectRatio
|
|
||||||
? { aspectRatio: `${asset.aspectRatio}` }
|
|
||||||
: {}),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={asset.thumbUrl}
|
|
||||||
alt=""
|
|
||||||
fill
|
|
||||||
sizes="(max-width: 600px) 50vw, (max-width: 1000px) 33vw, 25vw"
|
|
||||||
loading={index < 6 ? 'eager' : 'lazy'}
|
|
||||||
{...(asset.blurDataURL
|
|
||||||
? { placeholder: 'blur' as const, blurDataURL: asset.blurDataURL }
|
|
||||||
: {})}
|
|
||||||
/>
|
|
||||||
{(asset.camera || asset.lens) && (
|
|
||||||
<div className="photo-grid__item-exif">
|
|
||||||
{[asset.camera, asset.lens, asset.focalLength].filter(Boolean).join(' · ')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FadeIn>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{lightboxIndex !== null && (
|
{lightboxIndex !== null && (
|
||||||
|
|||||||
Reference in New Issue
Block a user