Largest Contentful Paint (LCP) measures how quickly the main content loads. Since PayloadCMS is headless, LCP optimization focuses on your frontend framework (typically Next.js or React).
What is LCP?
Largest Contentful Paint (LCP) is the time it takes for the largest visible element to load and render.
LCP Thresholds
- Good: 2.5 seconds or less
- Needs Improvement: 2.5 - 4.0 seconds
- Poor: More than 4.0 seconds
Common LCP Elements
- Hero images
- Featured images on blog posts
- Large text blocks
- Video thumbnails
Diagnosing LCP Issues
Step 1: Measure Your LCP
Using PageSpeed Insights:
- Go to PageSpeed Insights
- Enter your site URL
- Look for LCP in "Core Web Vitals"
- Note which element is the LCP element
Using Chrome DevTools:
Next.js Image Optimization
Use Next.js Image Component
PayloadCMS stores images which you should serve through Next.js Image component:
File: components/PayloadImage.js
import Image from 'next/image';
export default function PayloadImage({ src, alt, width, height, priority = false }) {
// Construct Payload image URL
const imageUrl = `${process.env.NEXT_PUBLIC_PAYLOAD_URL}${src}`;
return (
<Image
src={imageUrl}
alt={alt}
width={width}
height={height}
priority={priority} // Set true for LCP images
placeholder="blur"
blurDataURL={`${imageUrl}?width=20`}
/>
);
}
Prioritize LCP Images
For hero/featured images (LCP elements):
// pages/index.js
import PayloadImage from '../components/PayloadImage';
export default function Home({ hero }) {
return (
<PayloadImage
src={hero.image.url}
alt={hero.image.alt}
width={1920}
height={1080}
priority={true} // Tells Next.js to preload this image
/>
);
}
Payload Image Field Configuration
Optimize Image Sizes in Payload Config
File: collections/Media.ts
import { CollectionConfig } from 'payload/types';
const Media: CollectionConfig = {
slug: 'media',
upload: {
staticURL: '/media',
staticDir: 'media',
imageSizes: [
{
name: 'thumbnail',
width: 400,
height: 300,
position: 'centre',
},
{
name: 'card',
width: 768,
height: 1024,
position: 'centre',
},
{
name: 'hero',
width: 1920,
height: 1080,
position: 'centre',
},
],
mimeTypes: ['image/*'],
},
fields: [],
};
export default Media;
Use Appropriate Image Sizes
// Fetch appropriate size from Payload
export default function BlogPost({ post }) {
// Use 'hero' size for featured image
const imageUrl = post.featuredImage.sizes?.hero?.url || post.featuredImage.url;
return (
<Image
src={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}${imageUrl}`}
alt={post.featuredImage.alt}
width={1920}
height={1080}
priority={true}
/>
);
}
Server-Side Rendering Optimization
Static Site Generation (SSG)
Generate pages at build time for best performance:
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
const res = await fetch(
`${process.env.PAYLOAD_URL}/api/posts?where[slug][equals]=${params.slug}`
);
const data = await res.json();
return {
props: {
post: data.docs[0],
},
revalidate: 60, // ISR: Regenerate page every 60 seconds
};
}
export async function getStaticPaths() {
const res = await fetch(`${process.env.PAYLOAD_URL}/api/posts`);
const data = await res.json();
const paths = data.docs.map(post => ({
params: { slug: post.slug },
}));
return {
paths,
fallback: 'blocking',
};
}
Server-Side Rendering (SSR)
For dynamic content:
// pages/blog/index.js
export async function getServerSideProps() {
const res = await fetch(`${process.env.PAYLOAD_URL}/api/posts?sort=-publishedDate&limit=10`);
const data = await res.json();
return {
props: {
posts: data.docs,
},
};
}
Preload Critical Assets
Preload Hero Images
// pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head>
{/* Preload hero image */}
<link
rel="preload"
as="image"
href="/path-to-hero-image.jpg"
imageSrcSet="/hero-small.jpg 400w, /hero-medium.jpg 800w, /hero-large.jpg 1920w"
imageSizes="100vw"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
API Response Optimization
Limit Payload API Responses
Only fetch needed fields:
// Instead of fetching all fields
const res = await fetch(`${process.env.PAYLOAD_URL}/api/posts/${id}`);
// Fetch only needed fields
const res = await fetch(
`${process.env.PAYLOAD_URL}/api/posts/${id}?depth=0&select=title,slug,featuredImage`
);
Use Payload Local API (Server-Side)
Faster than HTTP requests:
// Server-side only
import payload from 'payload';
export async function getStaticProps({ params }) {
await payload.init({
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URI,
local: true,
});
const posts = await payload.find({
collection: 'posts',
where: {
slug: {
equals: params.slug,
},
},
});
return {
props: {
post: posts.docs[0],
},
};
}
Caching Strategies
Enable Next.js Cache Headers
File: next.config.js
module.exports = {
async headers() {
return [
{
source: '/media/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
};
Use SWR for Client-Side Data
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((res) => res.json());
export default function Posts() {
const { data, error } = useSWR(
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/posts`,
fetcher,
{
revalidateOnFocus: false,
dedupingInterval: 60000, // 1 minute
}
);
if (error) return <div>Failed to load</div>;
if (!data) return <div>Loading...</div>;
return (
<div>
{data.docs.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
);
}
Font Optimization
Use Next.js Font Optimization
// pages/_app.js
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // Prevent FOIT (Flash of Invisible Text)
});
export default function MyApp({ Component, pageProps }) {
return (
<main className={inter.className}>
<Component {...pageProps} />
</main>
);
}
Code Splitting
Dynamic Imports for Non-Critical Components
import dynamic from 'next/dynamic';
// Load non-critical components lazily
const Comments = dynamic(() => import('../components/Comments'), {
loading: () => <p>Loading comments...</p>,
ssr: false, // Don't render on server
});
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* Comments load only when needed */}
<Comments postId={post.id} />
</article>
);
}
Reduce JavaScript Bundle Size
Analyze Bundle
npm install @next/bundle-analyzer
File: next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// your config
});
Run analysis:
ANALYZE=true npm run build
Tree Shaking and Dead Code Elimination
Ensure imports are specific:
// Bad - imports entire library
import _ from 'lodash';
// Good - imports only needed function
import debounce from 'lodash/debounce';
CDN and Hosting
Use Vercel or Netlify
Both optimize Next.js automatically:
- Edge caching
- Image optimization
- Automatic compression
Self-Hosting Optimization
If self-hosting, ensure:
- Gzip/Brotli compression enabled
- HTTP/2 enabled
- CDN configured for static assets
Testing & Monitoring
Test with Lighthouse
npm install -g lighthouse
lighthouse https://yoursite.com --view
Use Web Vitals Library
// pages/_app.js
export function reportWebVitals(metric) {
if (metric.label === 'web-vital') {
console.log(metric); // or send to analytics
// Send to GA4
if (window.gtag) {
window.gtag('event', metric.name, {
value: Math.round(metric.value),
event_label: metric.id,
non_interaction: true,
});
}
}
}
Common Issues
Issue: Payload Images Loading Slowly
Solution:
- Use Next.js Image component
- Configure image sizes in Payload
- Enable CDN for Payload media
- Consider external image service (Cloudinary, imgix)
Issue: Large JavaScript Bundle
Solution:
- Use dynamic imports
- Analyze bundle with Bundle Analyzer
- Remove unused dependencies
- Split code by route
Issue: Slow API Responses
Solution:
- Use Payload Local API server-side
- Implement caching
- Optimize database queries
- Use ISR instead of SSR