We recently migrated a client from WooCommerce to a custom Next.js storefront. The old site had a PageSpeed score of 61. Three weeks after launch, we hit 98. Here's exactly how.
The Baseline Problem
WooCommerce + unoptimized images + 47 jQuery plugins = PageSpeed nightmare. The client's old site had:
- 8.2MB of JavaScript
- 14MB of uncompressed images
- No server-side caching
- 6.2s Largest Contentful Paint (LCP)
Fix 1: Images (The Biggest Win)
The next/image component is genuinely excellent. Use it for everything.
import Image from "next/image";
// Before (regular img tag)
<img src="/jewelry/ring.jpg" alt="Diamond ring" />
// After (next/image with explicit dimensions)
<Image
src="/jewelry/ring.jpg"
alt="Diamond ring"
width={800}
height={800}
priority={isAboveFold}
sizes="(max-width: 768px) 100vw, 50vw"
/>
Key rules:
- Always set
priorityon the hero/LCP image - Use
sizesprop to serve the right image size per viewport - Store images in AWS S3 with CloudFront CDN, not your origin server
Fix 2: Font Loading
Google Fonts kill performance when loaded the old way. Next.js has a built-in solution:
// app/layout.tsx
import { Inter, Syne } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
variable: "--font-body",
display: "swap",
});
const syne = Syne({
subsets: ["latin"],
variable: "--font-display",
display: "swap",
weight: ["400", "700", "800"],
});
This downloads fonts at build time, self-hosts them, and eliminates the Google Fonts DNS lookup.
Fix 3: Bundle Analysis
Install @next/bundle-analyzer and actually look at what you're shipping:
ANALYZE=true npm run build
In our case, we found:
moment.js(67KB gzipped) replaced withdate-fns(tree-shakeable)- A full Lodash import replaced with individual function imports
- Three different icon libraries consolidated to Lucide
These cuts alone saved 180KB from the JS bundle.
Fix 4: Route-Level Code Splitting
Use dynamic imports for components that aren't needed on initial render:
import dynamic from "next/dynamic";
const VideoPlayer = dynamic(() => import("@/components/VideoPlayer"), {
loading: () => <div className="skeleton h-64 w-full rounded-xl" />,
ssr: false, // Video players don't need SSR
});
const ProductReviews = dynamic(() => import("@/components/ProductReviews"));
Fix 5: React Server Components
The biggest architectural win in Next.js 13+. Anything that doesn't need interactivity should be a Server Component:
// This runs on the server no JS sent to client
async function ProductPage({ params }: { params: { slug: string } }) {
const product = await getProduct(params.slug); // Direct DB call
return (
<div>
<ProductInfo product={product} /> {/* Server Component */}
<AddToCartButton productId={product.id} /> {/* Client Component */}
</div>
);
}
Pushing interactivity to the leaves of your component tree is the single most impactful pattern for performance.
Fix 6: Edge Caching with ISR
For product pages that change occasionally:
export const revalidate = 3600; // Revalidate every hour
// Or on-demand revalidation via webhook
import { revalidatePath } from "next/cache";
export async function POST() {
revalidatePath("/products/[slug]");
}
The Results
| Metric | Before | After |
|---|---|---|
| PageSpeed Score | 61 | 98 |
| LCP | 6.2s | 1.1s |
| Total Blocking Time | 840ms | 45ms |
| JS Bundle Size | 8.2MB | 310KB |
| Conversion Rate | 0.8% | 2.4% |
The conversion rate improvement was the real win for the client which is ultimately what performance optimization is for.
Building a Next.js project and want it fast from day one? Let's talk.

Written by Hariom Patil
Lead Frontend Engineer at Axom Infotech

