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
Table of Contents
- 1. Lazy Loading Components That Are Not Needed Immediately
- 2. Optimizing Images Using Getimageprops and the <picture> Tag
- 3. Using Correct Heading Structure (Seo & Accessibility)
- 4. Prioritizing Critical Resources (priority, fetchPriority)
- 5. Migrating Local Fonts to Next/font/local (Eliminating Render-Blocking CSS)
- 6. Semantic HTML and Aria Attributes
- 7. Skeleton Loaders and React Suspense
- 8. Optimizing Heavy Video Assets (Hls Streaming)
- Final Thoughts
View Raw (for LLMs)
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:
-
One large image served to all devices Mobile users ended up downloading desktop-sized images.
-
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:
- HTML loads
- CSS is downloaded and parsed
- Fonts are discovered late
- 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-labelusage for buttons - Accessible modals with focus trapping
- Meaningful
alttext 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.
- Chunking: We split the video into tiny 2–6 second segments (
.tsfiles). - Manifest File: The browser loads a lightweight
.m3u8playlist first. - 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
posterattribute 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.