Profile photo
Abhishek Baiju
7 min read

How I Improved Page Load & SEO for an Image-Heavy Next.js Application

Steps I followed to to improve the performance and seo of my application

#seo#performance#nextjs

Recently, I worked on an application where the homepage was extremely heavy:

  • Large image sections
  • Framer Motion animations across the page
  • A hero slider with 4 videos (1–3 minutes each)
  • Multiple modals, forms, and embedded iframes

After finishing the build, I ran Lighthouse and Google PageSpeed Insights — and the results were bad. Poor LCP, weak SEO score, slow initial load, and Google’s PageSpeed Index clearly flagged the page as problematic.

This article documents the exact steps I followed, why they mattered, and how they improved performance and SEO.


1. Lazy Loading Components That Are Not Needed Immediately

The Problem

Not everything on a page needs to be loaded upfront.

In my case, the homepage included:

  • Enquiry modals
  • A booking modal
  • A Google Calendar iframe

These components were bundled and shipped even if the user never interacted with them, which meant:

  • Larger JavaScript bundles
  • Slower hydration
  • Worse initial load performance

The Fix

I used dynamic imports and disabled SSR for components that were only required after user interaction.

import dynamic from "next/dynamic";
 
const WhatsappMe = dynamic(
  () => import("@/app/home/hero/components/WhatsappMe"),
  { ssr: false }
);
 
const BookAppointmentModal = dynamic(
  () => import("@/components/BookAppointmentModal"),
  { ssr: false }
);

Why This Helped

  • These components are fetched only when required
  • Reduced initial JavaScript payload
  • Faster Time to Interactive (TTI)

Lazy loading is not just an optimization — it’s a design decision. If the user hasn’t asked for it, don’t ship it.

2. Optimizing Images Using Getimageprops and the <picture> Tag

The Problem (Before)

The homepage relied heavily on large images and sliders. Initially, this caused two major issues:

  1. One large image served to all devices Mobile users ended up downloading desktop-sized images.

  2. CSS-based hiding (hidden, display: none) Even when images were visually hidden, browsers often downloaded both versions.

This heavily impacted Largest Contentful Paint (LCP) and mobile performance.

The Solution

I used getImageProps from Next.js to manually construct a proper <picture> element.

The approach:

  • Generate optimized props for mobile images
  • Generate optimized props for desktop images
  • Let the browser decide which image to download
<picture>
  <source media="(max-width: 768px)" srcSet={mobile.srcSet} />
  <img src={desktop.src} srcSet={desktop.srcSet} {...rest} />
</picture>

Why <picture> Works Better

Browsers evaluate <source> tags before making network requests.

  • Mobile users download only the mobile image
  • Desktop users download the high-quality image
  • No wasted bandwidth

The Result

  • Significant reduction in data usage on mobile
  • Faster LCP
  • Better PageSpeed score

This was a critical fix for an image-heavy homepage.

3. Using Correct Heading Structure (Seo & Accessibility)

This is simple but frequently done wrong.

What I Fixed

  • Only one <h1> per page
  • Major sections use <h2>
  • Subsections use <h3>

Why This Matters

  • Search engines understand page hierarchy better
  • Screen readers can navigate the content correctly
  • Improved SEO and accessibility scores

Headings are not styling tools. They define the document structure.

4. Prioritizing Critical Resources (priority, fetchPriority)

Not all assets deserve the same importance.

What I Did

  • Marked above-the-fold hero images with priority
  • Used fetchPriority="high" for critical visuals
  • Left non-critical assets as lazy-loaded

Why This Helped

  • Browser fetches key resources first
  • Faster Largest Contentful Paint (LCP)
  • Reduced competition between critical and non-critical assets

Overusing priority defeats its purpose, so it must be applied carefully.

5. Migrating Local Fonts to Next/font/local (Eliminating Render-Blocking CSS)

Fonts turned out to be one of the highest-impact optimizations in this project.

The Problem (Before the Fix)

Initially, custom fonts were defined in globals.css using @font-face.

/* globals.css (OLD) */
@font-face {
  font-family: "Gilroy";
  src: url("/fonts/Gilroy-Regular.woff2") format("woff2");
  font-weight: 400;
}
 
@font-face {
  font-family: "Gilroy";
  src: url("/fonts/Gilroy-Bold.woff2") format("woff2");
  font-weight: 700;
}

Tailwind configuration:

// tailwind.config.js (OLD)
theme: {
  extend: {
    fontFamily: {
      gilroy: ["Gilroy", "sans-serif"],
    },
  },
}

Why This Was a Performance Issue

This created a render-blocking chain:

  1. HTML loads
  2. CSS is downloaded and parsed
  3. Fonts are discovered late
  4. Font files start downloading

Consequences:

  • Flash of Invisible Text (FOIT)
  • Delayed First Contentful Paint (FCP)
  • Worse Largest Contentful Paint (LCP)

The Fix: Next/font/local

I migrated font handling to Next.js’s built-in font optimization.

Step 1: Define Fonts in JavaScript

// lib/fonts.ts
import localFont from "next/font/local";
 
export const gilroy = localFont({
  src: [
    {
      path: "../public/fonts/Gilroy-Regular.woff2",
      weight: "400",
    },
    {
      path: "../public/fonts/Gilroy-Bold.woff2",
      weight: "700",
    },
  ],
  variable: "--font-gilroy",
  display: "swap",
});

Step 2: Inject Font Variables in layout.tsx

// app/layout.tsx
import { gilroy } from "@/lib/fonts";
 
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={gilroy.variable}>{children}</body>
    </html>
  );
}

Step 3: Update Tailwind to Use CSS Variables

// tailwind.config.js (NEW)
theme: {
  extend: {
    fontFamily: {
      gilroy: ["var(--font-gilroy)", "sans-serif"],
    },
  },
}

What Changed after This Fix

  • Fonts preload automatically
  • Font loading happens in parallel with CSS
  • No FOIT
  • No layout shift
  • Improved FCP and LCP

This change alone had a noticeable impact on perceived performance.

6. Semantic HTML and Aria Attributes

Performance is not just about speed — it’s also about usability.

What I Focused On

  • Semantic tags (header, main, section, nav)
  • Proper aria-label usage for buttons
  • Accessible modals with focus trapping
  • Meaningful alt text for images

Why This Matters

  • Improved accessibility score
  • Better SEO signals
  • Cleaner DOM structure
  • Better experience for assistive technologies

Search engines reward well-structured, accessible pages.

7. Skeleton Loaders and React Suspense

Instead of showing blank screens, I focused on perceived performance.

What I Did

  • Used skeleton loaders for async content
  • Wrapped non-critical sections in Suspense
  • Streamed UI progressively

Why This Helped

  • Users see content immediately
  • App feels faster even if data is still loading
  • Reduced bounce rate

8. Optimizing Heavy Video Assets (Hls Streaming)

Video files are notoriously heavy. Serving standard MP4 files can kill bandwidth and slow down initial page loads.

The Problem

  • Massive Downloads: Users had to download the entire video file before reliable playback could start.
  • Wasted Bandwidth: If a user scrolled past the video after 5 seconds, they still downloaded the full 2-minute file.
  • Buffering: Poor connections caused stalling.

The Solution: HTTP Live Streaming (Hls)

We implemented HLS (HTTP Live Streaming) using hls.js. instead of serving a single video file.

  1. Chunking: We split the video into tiny 2–6 second segments (.ts files).
  2. Manifest File: The browser loads a lightweight .m3u8 playlist first.
  3. Adaptive Streaming: The player requests chunks one by one.
// useVideoSlider.tsx (simplified)
import Hls from "hls.js";
 
useEffect(() => {
  if (Hls.isSupported()) {
    const hls = new Hls();
    // Optimization: Don't download 4K video for a tiny mobile screen
    hls.config.capLevelToPlayerSize = true;
    hls.loadSource(videoSource); // Loads the .m3u8 playlist
    hls.attachMedia(videoRef.current);
  }
}, [videoSource]);

Why This Helped

  • Instant Startup: The browser only downloads the first small chunk to start playing.
  • Bandwidth Efficiency: If a user leaves, no extra data is downloaded.
  • Adaptive Bitrate: The video quality adjusts automatically based on the user's internet speed.

Bonus

  • We load the preview image first and then when the video is ready, we play it.

  • Also we have poster attribute in the video tag. but using that we can't control the image loading for 2 different screens. and also we can't use the features of NextJS Image component.

Final Thoughts

Most performance issues aren’t caused by one big mistake. They happen due to many small decisions that accumulate over time.

Once I:

  • Reduced unnecessary JavaScript
  • Fixed image delivery
  • Optimized fonts
  • Improved structure and accessibility

Both PageSpeed and SEO scores improved significantly, and more importantly, the app felt fast.

Performance should never be an afterthought. It’s part of how you design and build applications.

Share this post