In early 2020, Google announced Core Web Vitals would become a ranking signal. We had a marketplace running on Create React App (client-side rendering). Our Lighthouse score was 41 on mobile. The migration to Next.js with SSR took three months and completely changed our search performance.
Why Client-Side Rendering Hurts E-Commerce
The CRA (CSR) page load sequence:
1. Browser receives empty HTML: <div id="root"></div>
2. Download React bundle: 340KB
3. Parse and execute JavaScript
4. Fetch API data (products, categories, prices)
5. Render content
Total: 4.2 seconds to First Contentful Paint on 4G
Google's crawler sees that empty <div> and has to execute JavaScript to see content. Google does execute JS, but with a delay - sometimes weeks for recrawl.
The conversion impact: Each 1-second delay in page load reduces conversions by 7% (Akamai research). At 4.2 seconds, we were leaving significant revenue on the table.
Server-Side Rendering vs Incremental Static Regeneration
Next.js gives you three rendering modes. Choosing correctly is the real decision.
getServerSideProps (SSR) - Render on every request
// pages/products/[id].js
export async function getServerSideProps({ params }) {
const product = await fetchProduct(params.id);
return { props: { product } };
}
export default function ProductPage({ product }) {
return <ProductDetails product={product} />;
}
Use when: Content changes frequently (stock levels, real-time prices, personalized content).
Cost: Every page request hits your server. Can't be cached by CDN.
getStaticProps + revalidate (ISR) - Best for most e-commerce pages
// Product page: update every 60 seconds
export async function getStaticProps({ params }) {
const product = await fetchProduct(params.id);
return {
props: { product },
revalidate: 60, // Regenerate at most once per minute
};
}
export async function getStaticPaths() {
const topProducts = await fetchTopProducts(1000); // Pre-render top 1000
return {
paths: topProducts.map(p => ({ params: { id: p.id.toString() } })),
fallback: 'blocking', // Other products: SSR on first request, then cached
};
}
Use when: Content is mostly stable but needs periodic updates.
Benefit: CDN-cacheable. First user after revalidation triggers background regeneration. All others get cached HTML.
getStaticProps (pure static) - For genuinely static content
// Blog posts, category landing pages, static content
export async function getStaticProps() {
const categories = await fetchCategories();
return {
props: { categories },
// No revalidate: regenerate only on next build
};
}
The 12 Changes That Moved Us From 41 to 97
1. SSR → HTML delivered on first byte
Before: <div id="root"></div>
After: Full HTML including all product data
2. Image optimization with next/image
// Before: regular img tag, no optimization
<img src={product.image} width="400" height="400" />
// After: automatic WebP conversion, lazy loading, size optimization
import Image from 'next/image';
<Image
src={product.image}
width={400}
height={400}
priority={isAboveFold} // Preload hero images
/>
next/image automatically serves WebP to supporting browsers (60% smaller than JPEG), adds loading="lazy" for below-fold images, and prevents layout shift with explicit dimensions.
3. Font loading with next/font (Next.js 13 backport approach in 2020: preconnect)
<!-- _document.js - preconnect to font origin -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
4. Code splitting - dynamic imports for non-critical components
import dynamic from 'next/dynamic';
// Don't load the review modal until the user clicks "Write Review"
const ReviewModal = dynamic(() => import('../components/ReviewModal'), {
loading: () => null,
ssr: false, // No need to SSR a modal
});
5. Eliminated layout shift (CLS)
Every image and ad slot needs explicit dimensions reserved. The browser can't know image size until it loads - so it reflows layout. Fix: always specify width and height.
// Before: CLS of 0.34 (poor)
<img src={banner.url} style={{ width: '100%' }} />
// After: CLS of 0.02 (good)
// next/image enforces aspect ratio via padding-bottom trick
<Image src={banner.url} width={1200} height={300} layout="responsive" />
6. Critical CSS inlined, rest deferred
Next.js handles this automatically for its styling solutions (CSS Modules, styled-jsx). We migrated from a global CSS file (375KB) to CSS Modules.
The Results
| Metric | Before (CRA/CSR) | After (Next.js ISR) |
|---|---|---|
| Lighthouse Mobile | 41 | 97 |
| First Contentful Paint | 4.2s | 0.8s |
| Largest Contentful Paint | 6.1s | 1.4s |
| CLS | 0.34 | 0.02 |
| Total Blocking Time | 890ms | 120ms |
| Organic traffic (3 months later) | baseline | +34% |
| Conversion rate | baseline | +18% |
The SEO improvement took 3 months to fully materialize - that's Google's crawl cycle. But the conversion improvement was immediate: users on slower connections (mobile, 4G) saw a dramatically faster page.
What ISR Changed for Our Team
Before: a product update meant "push to git, CI runs, full rebuild, deploy - 8 minutes." With ISR: product prices, stock levels, and descriptions update without a build. The marketing team can update content and it goes live within 60 seconds without engineering involvement.
In 2024, Next.js App Router with React Server Components takes these principles further. But ISR in Next.js 9/10 was already a significant leap over what came before.
Aunimeda builds modern web frontends - from single-page applications to complex multi-locale sites.
Contact us to discuss your frontend project. See also: Web Development, Corporate Website Development