I am struggling to improve my NextJS website performance since the Largest Contentful Paint element is loading too late. I understand why it is loading late, but I am not able to find a solution to render it earlier. LCP load speed
The page in question is the product page of a marketplace. The main React server component (in NextJs) is rendering most of the information, but not the image (the LCP) since I need some interactivity on it and I have separated it into a client component.
Imagine the that the server component is this one:
async function fetchListing(params) {
try {
// Bunch of code up here...
let listing = await Listing.findOne({ _id: params.id });
return listing;
} catch (err) {
console.log(err);
return null;
}
}
const ProductPage = async ({ params }) => {
let listing = await fetchListing(params); // Reusing fetchListing here
if (!listing) return <NotFoundSection />;
listing = JSON.parse(JSON.stringify(listing));
return (
<>
{/* Bunch of code up here.. */}
<div className={classes.maxWidth}>
<div className={classes.lastLink}>{listing.title}</div>
<div className={classes.topSection}>
<Carousel listing={listing} />
{/* Bunch more code down here.. */}
</div>
</div>
</>
);
};
export default ProductPage;
And the carousel component looks like this:
'use client';
import { useEffect, useState } from 'react';
import Image from 'next/image';
import classes from './Carousel.module.css';
import {
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronUp,
} from 'react-feather';
const Carousel = ({ listing }) => {
const [previewSet, setPreviewSet] = useState([]);
const [currentPage, setCurrentPage] = useState(0);
const [current, setCurrent] = useState(0);
const [idx, setIdx] = useState(0);
const [currentImg, setCurrentImg] = useState();
useEffect(() => {
let preview = [];
let i = 0;
if (currentPage === 0) {
preview = listing.images.slice(0, 6);
i = 0;
} else if (currentPage === 1) {
preview = listing.images.slice(6, 12);
i = 6;
} else if (currentPage === 2) {
preview = listing.images.slice(12, 18);
i = 12;
} else if (currentPage === 3) {
preview = listing.images.slice(18, 24);
i = 18;
}
setIdx(i);
setPreviewSet(preview);
}, [currentPage, listing]);
const nextHandler = () => {
if (current === listing.images.length - 1) {
setCurrent(0);
setCurrentPage(0);
setCurrentImg(listing.images[0]);
} else if (
(current === 5 && currentPage === 0) ||
(current === 11 && currentPage === 1) ||
(current === 17 && currentPage === 2) ||
(current === 23 && currentPage === 3)
) {
setCurrentPage(currentPage + 1);
setCurrent(current + 1);
} else setCurrent(current + 1);
};
const previousHandler = () => {
if (
(current === 6 && currentPage === 1) ||
(current === 12 && currentPage === 2) ||
(current === 18 && currentPage === 3) ||
(current === 24 && currentPage === 4)
) {
setCurrentPage(currentPage - 1);
setCurrent(current - 1);
} else {
setCurrent(current - 1);
}
};
const handleClick = (index) => {
setCurrent(index + idx);
};
useEffect(() => {
setCurrentImg(listing.images[current]);
}, [current, listing]);
const nextPage = () => {
setCurrentPage(currentPage + 1);
};
const previousPage = () => {
setCurrentPage(currentPage - 1);
};
return (
<>
<div className={classes.carouselContainer}>
<div className={classes.wrapper}>
<div className={classes.wrapperContainer}>
{currentPage !== 0 && (
<div className={classes.scroll} onClick={() => previousPage()}>
<ChevronUp className={classes.arrow} />
</div>
)}
</div>
<ul className={classes.imagePreview}>
{previewSet?.map((image, idx) => (
<li>
<Image
className={
currentImg === image ? classes.image : classes.nonActive
}
src={
image?.includes('imagekit.io')
? `${image}?tr=w-80,h-80,q-75`
: image
}
effect="blur"
alt={`${listing.title} - image ${idx}`}
width={80}
height={80}
onMouseEnter={() => handleClick(idx)}
/>
</li>
))}
</ul>
<div className={classes.wrapperContainer}>
{previewSet.length >= 6 && listing.images.length > 6 && (
<button className={classes.scroll} onClick={() => nextPage()}>
<ChevronDown className={classes.arrow} />
</button>
)}
</div>
</div>
<div className={classes.largeImagePreview}>
<div className={classes.next} onClick={() => nextHandler()}>
<ChevronRight className={classes.arrow} />
</div>
{current !== 0 && (
<div className={classes.previous} onClick={() => previousHandler()}>
<ChevronLeft className={classes.arrow} />
</div>
)}
<div className={classes.wrapp}>
<Image
priority={true}
src={`${listing.images[current]}?tr=w-876,h-1134,c-at_max_enlarge`}
className={
listing.status !== 'active'
? classes.imageSold
: classes.imageLarge
}
height={633}
width={474}
alt={`${listing.title} - image ${current}`}
/>
</div>
</div>
</div>
</>
);
};
export default Carousel;
The Carousel component will render to look something like this Carousel screenshot
Now, that <Image /> component with priority set to true is going to be my LCP.
I understand that my loading time for the LCP is long because this <Image /> component is loading in the client, much later than then the rest of the server rendered content which I did not paste in the ProductPage component here.
I know this also because I ran a test where I edited the Carousel component to remove all interactivity and make it a Server component, this improved the LCP loading speed dramatically.
My question is, how can I work around this? How can I render the Carousel component in the server, and then hydrate it in the client so that it can maintain its interactivity (scrolling images left and right)