The Business Case for Performance
Every 100ms improvement in page load time correlates with a measurable improvement in conversion rate. This is not a theoretical claim — Google, Amazon, and every major e-commerce platform has published data on this relationship. For a D2C brand doing ₹5 Crore annually, a 1% improvement in conversion rate from performance improvements is worth ₹5 Lakh per year.
The client we worked with had a bespoke e-commerce platform that was functionally excellent — custom product configurator, loyalty system, B2B portal — but loading in 4.2 seconds on a 4G connection. This post is the technical breakdown of how we got it to 0.9 seconds.
The Audit: What Was Actually Slow
Before optimising anything, we ran a comprehensive audit using Lighthouse, WebPageTest from multiple geographic locations, and Next.js Bundle Analyzer. The findings:
- First Contentful Paint: 2.8s. The server was rendering the page but the largest above-the-fold image was not being discovered until after JavaScript hydration.
- Total Blocking Time: 1,400ms. Three analytics and marketing scripts (Google Tag Manager, Facebook Pixel, Hotjar) were being loaded synchronously in the document head.
- Largest Contentful Paint: 4.2s. The hero image was 1.2MB, in PNG format, served from the origin server without CDN.
- JavaScript bundle: 2.1MB uncompressed. The product configurator was loading its entire 3D rendering library on every page, including pages that had no configurator.
- Database queries on every page load: The navigation menu was fetching product categories on every server-side render, with no caching.
Fix 1: Image Optimisation Pipeline
Every product image went through a Sharp-based processing pipeline on upload: conversion to WebP, generation of multiple sizes (320w, 640w, 1200w), and upload to S3 with a CloudFront distribution configured for aggressive caching (Cache-Control: max-age=31536000).
In the Next.js frontend, we replaced all <img> tags with next/image components with explicit width, height, and priority on above-the-fold images. The priority flag triggers a <link rel="preload"> in the document head, which starts the image download before the browser finishes parsing the page.
Result: hero image download from 4.2 seconds to 0.3 seconds. LCP dropped by 1.8 seconds.
Fix 2: Third-Party Script Loading Strategy
Google Tag Manager, Facebook Pixel, and Hotjar were loading synchronously in the <head>, blocking page rendering for 1,400ms of combined script parse and execution time.
The fix: Next.js's <Script> component with strategy="lazyOnload" for all analytics scripts. These scripts load after the page is interactive, meaning they have zero impact on Time to Interactive for the user. The trade-off: analytics events in the first few seconds of a page load may not be captured. For e-commerce conversion tracking, this is acceptable — the add-to-cart and checkout events fire well after the page loads.
Result: Total Blocking Time dropped from 1,400ms to 180ms.
Fix 3: Code Splitting for Heavy Components
The product configurator used a Three.js-based 3D rendering library that was 800KB of JavaScript. This was being included in the global bundle and downloaded on every page, including the homepage, blog, and account pages.
The fix: dynamic import with next/dynamic and ssr: false. The configurator component is only loaded when the user navigates to a product page that has the configurator enabled. Pages without the configurator have no knowledge that it exists.
const ProductConfigurator = dynamic(
() => import('@/components/ProductConfigurator'),
{ ssr: false, loading: () => <ConfiguratorSkeleton /> }
);
Result: JavaScript bundle on non-configurator pages dropped from 2.1MB to 320KB. Time to Interactive on the homepage dropped by 1.6 seconds.
Fix 4: Navigation Data Caching
The navigation menu fetched product categories from the database on every server-side render — approximately 200 database queries per minute at moderate traffic.
The fix: move category data to a React Server Component that runs at build time (for static pages) or is cached with a 5-minute revalidation window using Next.js's fetch cache. Navigation data changes at most a few times per day, so a 5-minute cache is appropriate.
// In a Server Component
const categories = await fetch('/api/categories', {
next: { revalidate: 300 } // Cache for 5 minutes
});
Result: 200 database queries per minute became 1 query every 5 minutes. Database CPU utilisation dropped from 45% to 8%.
Fix 5: Font Loading
The brand used a custom variable font loaded from their own CDN, declared in a global CSS file with @font-face. The font was not being preloaded, causing layout shift when it loaded after page render (CLS of 0.34).
The fix: use next/font/local to declare the font in the root layout, which automatically generates a <link rel="preload"> and applies font-display: swap. CLS dropped from 0.34 to 0.02.
The Results
After these five fixes, the performance metrics were:
- LCP: 4.2s → 0.9s
- TBT: 1,400ms → 180ms
- CLS: 0.34 → 0.02
- Lighthouse Performance Score: 31 → 94
The business result: conversion rate improved from 0.8% to 2.1% within 30 days of deploying the changes. At the same traffic volume, that represented an additional ₹80 Lakh in annual revenue attributable directly to performance improvements.
