Fix LCP Issues on Keystonejs (Loading Speed) | OpsBlu Docs

Fix LCP Issues on Keystonejs (Loading Speed)

Improve KeystoneJS LCP by optimizing GraphQL resolver performance, using next/image for media fields, and enabling ISR on content pages.

General Guide: See Global LCP Guide for universal concepts and fixes.

What is LCP?

Largest Contentful Paint measures when the largest content element becomes visible. Google recommends LCP under 2.5 seconds. KeystoneJS is a Node.js headless CMS with a GraphQL API backed by Prisma/PostgreSQL. LCP depends on GraphQL resolver performance, your frontend framework, and image handling.

KeystoneJS-Specific LCP Causes

  • N+1 GraphQL queries -- Keystone's auto-generated resolvers can trigger multiple database queries for nested relationships
  • No built-in image CDN -- Keystone's image field stores files locally or on S3 but provides no image processing pipeline
  • Client-side GraphQL fetching -- Apollo Client or urql queries in the browser create API waterfalls before LCP
  • Prisma query overhead -- complex queries with multiple relationships and filters can be slow without database indexing
  • Frontend hydration -- React/Next.js frontends consuming Keystone's API add hydration delay

Fixes

1. Use SSG/ISR with Keystone's GraphQL API

// Next.js fetching from Keystone's GraphQL endpoint at build time
const KEYSTONE_API = process.env.KEYSTONE_API_URL + '/api/graphql';

export async function getStaticProps({ params }) {
  const { data } = await fetch(KEYSTONE_API, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: `query GetPost($slug: String!) {
        post(where: { slug: $slug }) {
          title
          heroImage { url width height }
          content { document }
        }
      }`,
      variables: { slug: params.slug },
    }),
  }).then(r => r.json());

  return { props: { post: data.post }, revalidate: 60 };
}

2. Optimize Image Fields

Keystone's image field stores files but does not process them. Add a CDN with transforms:

// keystone.ts -- configure image field with S3 + CloudFront
import { config } from '@keystone-6/core';
import { s3Image } from '@keystone-6/core/fields';

export default config({
  storage: {
    heroImages: {
      kind: 's3',
      type: 'image',
      bucketName: process.env.S3_BUCKET,
      region: process.env.S3_REGION,
      // Serve via CloudFront CDN
      baseUrl: `${process.env.CLOUDFRONT_URL}/hero-images`,
    },
  },
  // ...
});

Then use CloudFront + Lambda@Edge for on-the-fly image resizing:

function optimizeKeystoneImage(url, width = 800) {
  // CloudFront image resize URL pattern
  return `${url}?width=${width}&quality=80&format=auto`;
}

3. Optimize GraphQL Resolvers

Add database indexes for commonly queried fields:

// keystone.ts -- add indexes for query performance
import { list } from '@keystone-6/core';
import { text, timestamp, relationship } from '@keystone-6/core/fields';

export const Post = list({
  fields: {
    slug: text({ isIndexed: 'unique' }),
    title: text(),
    publishedAt: timestamp({ isIndexed: true }),
    author: relationship({ ref: 'User.posts' }),
  },
  // Enable query limits to prevent expensive queries
  graphql: {
    maxTake: 50,
  },
});

4. Preload LCP Image

<Head>
  <link rel="preconnect" href={process.env.NEXT_PUBLIC_CDN_URL} />
  <link rel="preload" as="image"
        href={optimizeKeystoneImage(post.heroImage.url, 1920)} />
</Head>

5. Cache GraphQL Responses

// Apollo Server cache hints in Keystone's extended GraphQL
// Or use HTTP-level caching:
// Set Cache-Control headers on Keystone's API responses
app.use('/api/graphql', (req, res, next) => {
  if (req.method === 'GET') {
    res.setHeader('Cache-Control', 'public, max-age=300, s-maxage=3600');
  }
  next();
});

Measuring LCP on KeystoneJS

  1. Keystone Admin UI -- check content creation timestamps and relationship depth
  2. Prisma query logging -- enable log: ['query'] in Prisma client to see SQL execution times
  3. Chrome DevTools Network -- filter GraphQL requests to see payload sizes and response times
  4. Test page types: content pages, listing pages (with pagination), and pages with deep relationships

Analytics Script Impact

KeystoneJS is headless -- analytics integration is in your frontend. Use deferred loading via next/script or equivalent.