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

Fix LCP Issues on Tinacms (Loading Speed)

Reduce TinaCMS LCP by optimizing Git-backed media images, enabling Next.js static generation, and preloading above-fold MDX content.

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. TinaCMS is a Git-backed headless CMS that integrates with Next.js, Astro, and other SSG/SSR frameworks. Content is stored as Markdown/MDX in your Git repository, and images are stored in the repo's public/ directory or a connected media store (Cloudinary, S3). LCP depends on your framework's rendering strategy (SSG vs. SSR), image optimization pipeline, and TinaCMS client bundle overhead.

TinaCMS-Specific LCP Causes

  • TinaCMS client bundle in production -- the TinaCMS editing overlay JavaScript (~150KB+) loads on every page if not properly gated to admin/edit mode
  • Git-backed images without optimization -- images stored in public/uploads/ serve at original resolution with no CDN transforms
  • SSR instead of SSG -- pages using getServerSideProps instead of getStaticProps re-render on every request
  • Large MDX component bundles -- custom MDX components (interactive charts, embeds) add JavaScript that blocks rendering
  • No image CDN -- without Cloudinary or similar integration, images bypass any optimization pipeline

Fixes

1. Gate TinaCMS Client to Edit Mode

// In your Next.js _app.tsx or layout
import dynamic from 'next/dynamic';

// Only load TinaCMS in edit mode
const TinaProvider = dynamic(
  () => import('../.tina/components/TinaProvider'),
  { ssr: false }
);

export default function App({ Component, pageProps }) {
  const isEditMode = pageProps.__tina_editMode;

  if (isEditMode) {
    return (
      <TinaProvider>
        <Component {...pageProps} />
      </TinaProvider>
    );
  }

  return <Component {...pageProps} />;
}

2. Use Static Generation

// In your page component ([slug].tsx)
import { client } from '../.tina/__generated__/client';

// Use getStaticProps + getStaticPaths for SSG
export async function getStaticProps({ params }) {
  const { data } = await client.queries.post({
    relativePath: `${params.slug}.mdx`,
  });
  return { props: { data }, revalidate: 60 }; // ISR: revalidate every 60s
}

export async function getStaticPaths() {
  const { data } = await client.queries.postConnection();
  return {
    paths: data.postConnection.edges.map(edge => ({
      params: { slug: edge.node._sys.filename },
    })),
    fallback: 'blocking',
  };
}

3. Optimize Images

With Cloudinary media store:

// In your TinaCMS media config (tina/config.ts)
import { TinaCloudMediaStore } from 'tinacms-cloudinary';

export default defineConfig({
  media: {
    tina: {
      // Use Cloudinary for automatic optimization
      mediaRoot: 'uploads',
      publicFolder: 'public',
    },
  },
});

// In your component -- use Cloudinary transforms
const optimizedUrl = imageUrl.replace(
  '/upload/',
  '/upload/w_1200,q_80,f_auto/'
);

With Next.js Image component:

// In your MDX components or page templates
import Image from 'next/image';

export function HeroImage({ src, alt }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={1200}
      height={630}
      priority  // Preloads the LCP image
      quality={80}
      style={{ width: '100%', height: 'auto', objectFit: 'cover' }}
    />
  );
}

Without a framework image component:

# Optimize images in your repo before committing
find public/uploads -name "*.jpg" -exec mogrify -strip -quality 80 -resize "1920>" {} \;
find public/uploads -name "*.png" -exec optipng -o2 {} \;

4. Preload Critical Assets

// In your Next.js _document.tsx or layout <head>
import Head from 'next/head';

export function PageHead({ heroImage }) {
  return (
    <Head>
      {heroImage && (
        <link rel="preload" as="image" href={heroImage} />
      )}
      <link rel="preconnect" href="https://assets.tina.io" />
    </Head>
  );
}

5. Lazy-Load Heavy MDX Components

// In your MDX component mapping
import dynamic from 'next/dynamic';

const InteractiveChart = dynamic(
  () => import('../components/InteractiveChart'),
  { ssr: false, loading: () => <div style={{ minHeight: 400 }} /> }
);

export const mdxComponents = {
  InteractiveChart,
  // ... other components
};

Measuring LCP on TinaCMS Sites

  1. Next.js Analytics (if using Next.js) -- enable experimental.webVitalsAttribution for automatic Core Web Vitals reporting
  2. PageSpeed Insights -- test both the homepage and content-heavy MDX pages
  3. Check bundle size -- npx next-bundle-analyzer to verify TinaCMS client is not in production bundles
  4. Compare SSG vs. SSR -- pages using getStaticProps should have significantly lower TTFB than getServerSideProps

Analytics Script Impact

  • TinaCMS itself has no analytics impact in production (it is a build-time/edit-time tool)
  • Place analytics scripts after framework hydration completes
  • Use Next.js Script component with strategy="lazyOnload" for non-critical tracking
  • If using Astro, place scripts with is:inline or defer in your layout