Ömer
Özbay

Full-Stack Engineer

LOADING
©2026
7 Critical Performance Problems I Fixed Before SEO in My Next.js Project
ARCHITECTURENext.jsPerformance

7 Critical Performance Problems I Fixed Before SEO in My Next.js Project

calendar_todayJUL 13, 2026
schedule7 MIN READ
boltADVANCED LEVEL

7 Critical Performance Problems I Fixed Before SEO in My Next.js Project

Most developers treat SEO as the primary gateway to organic traffic. They spend hours crafting meta tags, sitemap structures, and canonical links — while ignoring the engine underneath: performance.

Here is the uncomfortable truth I discovered while building gucluyumhe.dev: Google will not rank a page that performs poorly, regardless of how perfect your SEO is. Core Web Vitals are a direct ranking signal. A slow LCP, a high CLS, or a heavy TBT score directly undermines your SEO efforts before they even start.

As a Senior Full Stack Architect, I fixed these 7 performance problems in my Next.js project before touching a single SEO tag. Here is exactly what I did and why.


The Starting Point: A PageSpeed Score of 61

My first Lighthouse audit returned a mobile PageSpeed score of 61 out of 100. The diagnostics pointed to specific, fixable architectural problems.

Performance:      61
Accessibility:    92
Best Practices:   96
SEO:              88

Fixing performance first brought me to 100 across the board. Then SEO was straightforward.


Problem 1: Render-Blocking Third-Party Scripts

What Happened

The first version of my site loaded Google Analytics synchronously. This blocked the main thread during the critical rendering path, causing Total Blocking Time (TBT) to spike.

The Fix: Deferred Script Loading via @next/third-parties

// WRONG — synchronous script blocks the main thread
<Script src="https://www.googletagmanager.com/gtag/js?id=G-XXXX" />

// CORRECT — load only during idle time
import { GoogleAnalytics } from '@next/third-parties/google';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID!} />
      </body>
    </html>
  );
}

Impact: TBT dropped from 340ms to 0ms.


Problem 2: Unoptimized Cover Images Destroying LCP

What Happened

Blog post cover images were loaded as raw <img> tags from Unsplash. On mobile, a 2MB JPEG was being downloaded before anything else rendered — destroying my Largest Contentful Paint (LCP) score.

The Fix: Next.js Image Component with priority

import Image from 'next/image';

// Hero image: always set priority for above-the-fold images
<Image
  src={post.coverImage}
  alt={post.title}
  width={1200}
  height={630}
  priority={true}
  sizes="(max-width: 768px) 100vw, 1200px"
  style={{ objectFit: 'cover' }}
/>

// Below-the-fold images: lazy load by default
<Image
  src={relatedPost.coverImage}
  alt={relatedPost.title}
  width={400}
  height={210}
  loading="lazy"
/>

Impact: LCP improved from 4.2s to 1.1s on mobile.


Problem 3: Client Components Shipping Unnecessary JavaScript

What Happened

I had wrapped entire page sections in 'use client' directives just because one small child component needed useState. This caused Next.js to ship the entire React rendering tree to the browser as JavaScript.

The Fix: Push use client to the Leaf Node

// WRONG — entire section becomes a client bundle
'use client';
export default function BlogSection() {
  const [liked, setLiked] = useState(false);
  return (
    <section>
      <h2>Latest Posts</h2>
      <PostList posts={posts} />
      <LikeButton liked={liked} onLike={() => setLiked(true)} />
    </section>
  );
}

// CORRECT — server component wraps a minimal client component
// BlogSection.tsx (Server Component — zero JavaScript shipped)
export default function BlogSection({ posts }: Props) {
  return (
    <section>
      <h2>Latest Posts</h2>
      <PostList posts={posts} />
      <LikeButton />
    </section>
  );
}

// LikeButton.tsx (Client Component — minimal JavaScript)
'use client';
export function LikeButton() {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(true)}>{liked ? 'Liked' : 'Like'}</button>;
}

Impact: JavaScript bundle reduced by approximately 67kb.


Problem 4: Layout Shift from Dynamically Loaded Fonts

What Happened

Google Fonts were loaded asynchronously. On first paint, fallback fonts rendered, then the custom font loaded — causing a massive Cumulative Layout Shift (CLS) as text reflowed.

The Fix: next/font with display swap and Size Preloading

// app/layout.tsx
import { Inter, JetBrains_Mono } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  preload: true,
  variable: '--font-inter',
});

const jetbrainsMono = JetBrains_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-mono',
  weight: ['400', '700'],
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
      <body>{children}</body>
    </html>
  );
}

Impact: CLS dropped from 0.18 to 0.00.


Problem 5: Static Pages Not Being Pre-Rendered

What Happened

My blog post pages were using fetch() at request time on every visit, even though the content was markdown files that only change on deploy. This made every page load slower than necessary.

The Fix: generateStaticParams and Static Markdown Parsing

// app/blog/[slug]/page.tsx

// Pre-generate all blog post routes at build time
export async function generateStaticParams(): Promise<{ slug: string }[]> {
  const posts = await getAllPostSlugs();
  return posts.map((slug) => ({ slug }));
}

// This page is served as a static HTML file — zero server overhead
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);

  if (!post) notFound();

  return <ArticleLayout post={post} />;
}

Impact: Time to First Byte (TTFB) dropped from 320ms to 18ms, served from CDN edge.


Problem 6: Missing rel="preconnect" for External Origins

What Happened

The browser was establishing cold TCP/TLS connections to Unsplash and Google Fonts at render time. Each connection added 150–300ms of network latency before a single byte of content arrived.

The Fix: Preconnect Hints in the Document Head

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <link rel="preconnect" href="https://images.unsplash.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
        <link rel="dns-prefetch" href="https://www.google-analytics.com" />
      </head>
      <body>{children}</body>
    </html>
  );
}

Impact: External resource load time reduced by approximately 240ms on average.


Problem 7: No Caching Strategy for API Routes

What Happened

I had an /api/posts route that read and parsed all markdown files on every request. With multiple visitors hitting the page simultaneously, this caused redundant file I/O on every call.

The Fix: In-Memory Cache with Revalidation

// lib/posts-cache.ts
interface CacheEntry<T> {
  data: T;
  cachedAt: number;
}

const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
const cache = new Map<string, CacheEntry<unknown>>();

export async function getCached<T>(
  key: string,
  fetcher: () => Promise<T>
): Promise<T> {
  const entry = cache.get(key) as CacheEntry<T> | undefined;
  const now = Date.now();

  if (entry && now - entry.cachedAt < CACHE_TTL_MS) {
    return entry.data;
  }

  const data = await fetcher();
  cache.set(key, { data, cachedAt: now });
  return data;
}

// Usage in API route
export async function GET() {
  try {
    const posts = await getCached('all-posts', () => getAllPosts());
    return Response.json(posts);
  } catch (error) {
    console.error('Failed to fetch posts:', error);
    return Response.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

Impact: API response time reduced from approximately 180ms to 3ms on cache hits.


Final Score: 100 Across the Board

After fixing all 7 problems, the Lighthouse audit results:

Performance:      100
Accessibility:    100
Best Practices:   100
SEO:              100

This is when SEO work actually starts to matter. All the structured data, sitemaps, and Open Graph tags added after fixing performance were fully indexed and ranked because the technical foundation was solid.


The Core Insight

Performance is SEO. Core Web Vitals — LCP, CLS, TBT — are Google ranking signals. A site with a perfect sitemap and rich schema markup but a 60/100 PageSpeed score will never outrank a well-performing competitor.

The correct order of operations for any Next.js project:

1. Fix rendering architecture (RSC vs Client Components)
2. Optimize images (next/image, priority, sizes)
3. Eliminate layout shifts (next/font, reserved dimensions)
4. Defer non-critical scripts (third-parties, analytics)
5. Pre-render static content (generateStaticParams, ISR)
6. Add preconnect hints for external origins
7. Implement caching for dynamic data
8. Start SEO metadata, structured data, and sitemaps

Follow this sequence, and your SEO efforts will have the performance foundation they need to actually rank.

Ömer Özbay
Written By

Ömer Özbay

Full-Stack Engineer specialized in bridging high-performance backend architectures with pixel-perfect frontend experiences. Building the future with AI and modern web technologies.

7 Critical Performance Problems I Fixed Before SEO in My Next.js Project | Ömer Özbay Blog | Ömer Özbay