AboutBlogContact
Frontend EngineeringAugust 14, 2020 5 min read 180Updated: June 22, 2026

Next.js SSR vs CSR: How We Got 97 on Lighthouse for a High-Traffic Marketplace

AunimedaAunimeda
📋 Table of Contents

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

Read Also

React vs Angular 2: Why We Chose React for Our SaaS CRM in 2016aunimeda
Frontend Engineering

React vs Angular 2: Why We Chose React for Our SaaS CRM in 2016

After evaluating both for a complex CRM build, we chose React + Redux. The honest comparison: two-way binding vs unidirectional data flow, component philosophy, and what actually mattered at scale.

How to Set Up React with Webpack 1.x Without Create React App (2015)aunimeda
Frontend Engineering

How to Set Up React with Webpack 1.x Without Create React App (2015)

In 2015, Create React App didn't exist. Setting up React meant manually configuring Webpack 1.x, Babel 5, and Hot Module Replacement. Here's the exact working config we used in production - and why each piece was necessary.

Next.js 13/14: Server Actions and the New App Router (2023)aunimeda
Frontend Engineering

Next.js 13/14: Server Actions and the New App Router (2023)

React is coming full circle. In 2023, Next.js Server Actions are bringing back the simplicity of PHP and Ruby on Rails with modern React components.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Web Development

Get Consultation All articles