Fix PayloadCMS LCP Issues | OpsBlu Docs

Fix PayloadCMS LCP Issues

Improve PayloadCMS LCP by using Next.js Image for media uploads, optimizing collection queries, and enabling server-side rendering.

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:

  1. Go to PageSpeed Insights
  2. Enter your site URL
  3. Look for LCP in "Core Web Vitals"
  4. Note which element is the LCP element

Using Chrome DevTools:

  1. Open DevTools (F12)
  2. Go to Performance tab
  3. Click Record and reload page
  4. Look for "LCP" in timeline

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:


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:

  1. Use Next.js Image component
  2. Configure image sizes in Payload
  3. Enable CDN for Payload media
  4. Consider external image service (Cloudinary, imgix)

Issue: Large JavaScript Bundle

Solution:

  1. Use dynamic imports
  2. Analyze bundle with Bundle Analyzer
  3. Remove unused dependencies
  4. Split code by route

Issue: Slow API Responses

Solution:

  1. Use Payload Local API server-side
  2. Implement caching
  3. Optimize database queries
  4. Use ISR instead of SSR

Next Steps


Additional Resources