Last quarter, our flagship product’s dashboard had ballooned to 6.2 seconds average load time on a mid-tier laptop with a normal 4G connection. Users were frustrated, sales teams were fielding complaints, and analytics showed a 21% bounce rate on initial login.
We decided to commit a full performance sprint to fix it, with a goal of sub-1 second perceived load. Here’s exactly what we did, line by line, decision by decision.
Step 1: Get the Evidence
We ran:
✅ Lighthouse — to benchmark first paint, LCP, and blocking time
✅ WebPageTest — to verify real network throttling
✅ Chrome DevTools — for the flamegraph and request waterfall
The evidence:
3.4MB of JS bundles shipping on the initial route
1.2MB of images in hero sections, no compression
1.1s of third-party marketing scripts blocking main thread
22 separate API calls to hydrate dashboard widgets
CSS (via a giant Tailwind build) blocking render with no code splitting
Step 2: Slice the JavaScript
We migrated from Create React App to Next.js for better dynamic routing. Instead of shipping the entire dashboard on first load, we used next/dynamic
to load feature panels on demand.
Before:
After (Next.js with dynamic imports):
Impact: main bundle went from 3.4MB → 950KB.
Action taken:
Migrated to Next.js with
dynamic()
route-based code splittingSplit major feature routes into separate chunks
Removed Moment.js (600KB) in favor of Day.js (10KB)
Pruned lodash usage with explicit imports (
import pick from 'lodash/pick'
)
Result:
→ JS bundle shrank from 3.4MB to 950KB compressed
Step 3: Fix the Images
Our hero images were 2000px-wide JPEGs, served to every device. We switched to WebP with CloudFront and used srcset
.
Before:
After:
Plus, we added Sharp to compress images at build time:
Impact: hero image size dropped from 600KB → 120KB.
Action taken:
Added WebP generation via Sharp in our build pipeline
Used
srcset
for responsive imagesAdded lazy loading for below-the-fold assets
Shifted CDN from vanilla S3 to CloudFront with brotli compression
Result:
→ Image payload dropped from 1.2MB to 320KB, no visible quality loss
Step 4: Optimize the CSS
We had a giant Tailwind CSS file loaded globally. Instead, we used critical CSS inline and deferred the rest:
In _document.js
:
Then, for the remainder:
Impact: CSS blocking dropped from 600ms → 80ms.
Action taken:
Added
@tailwindcss/purge
to remove unused classesSplit Tailwind’s output by route
Loaded critical CSS inline for above-the-fold content
Deferred the rest of the CSS with
media="print"
+ onload swap
Result:
→ CSS blocking time reduced from 600ms to 80ms
Step 5: Consolidate API Calls
We had 22 independent API requests on first paint. We rewrote them as a single GraphQL query:
Before (pseudo-code):
After (GraphQL):
In React:
Impact: reduced first-load data requests from 22 → 4, massively cutting network overhead.
Action taken:
Audited queries in the dashboard with product managers
Collapsed multiple endpoints into a single GraphQL query
Added server-side caching for rarely changing metadata
Added
stale-while-revalidate
caching on the client
Step 6: Audit Third-Party Scripts
We found 11 third-party scripts running on page load. Many were legacy.
Steps taken:
✅ Moved non-critical tools (e.g., Hotjar) to on-demand loading
✅ Removed abandoned chat widgets
✅ Added a CI budget test for total script weight
In index.html
:
And moved expensive scripts behind a user click:
Impact: third-party blocking time dropped from 1.1s → 200ms.
The Measured Outcome
After deploying these changes:
✅ First Contentful Paint: 0.9s (was 4.3s)
✅ Largest Contentful Paint: 1.2s (was 6.2s)
✅ Total blocking time: 90% reduction
✅ JavaScript total: under 1MB
✅ Images total: under 400KB
✅ API latency: median response 220ms (down from 1.4s)
Customer feedback improved dramatically, conversion on the dashboard’s upsell CTA rose by 15%, and support tickets about “the app is slow” went to near zero.
Final Reflections
Speed improvements are never “one fix.” They’re a systematic series of engineering decisions — and real code:
Dynamic routing
Image pipelines
GraphQL batching
Critical path CSS
Smarter third-party loading
If you’re fighting 6-second loads, measure carefully, prioritize ruthlessly, and verify every change in production..
NEVER MISS A THING!
Subscribe and get freshly baked articles. Join the community!
Join the newsletter to receive the latest updates in your inbox.