Performance isn’t a feature—it’s a fundamental part of user experience. After optimizing applications that loaded in 8 seconds down to under 2, I’ve learned that performance optimization is as much about strategy as it is about tools. Here’s what actually moves the needle.
My Experience
I inherited a dashboard application that had everything: complex charts, real-time data, interactive features. It also took 12 seconds to become interactive. Users complained constantly.
The common wisdom said “optimize images” and “lazy load.” We did both. It helped a little—not enough.
The real problem was JavaScript. We had 2MB of JavaScript that blocked rendering. Once I understood that, everything changed. We cut JavaScript by 70%, implemented proper code splitting, and got first contentful paint under 1.5 seconds.
The lesson: measure first, then optimize where it matters.
Core Web Vitals Explained
Understanding what to optimize starts with Core Web Vitals:
LCP (Largest Contentful Paint)
Time for the largest element to render:
- Good: Under 2.5 seconds
- Needs Improvement: 2.5-4 seconds
- Poor: Over 4 seconds
INP (Interaction to Next Paint)
Time for the page to respond to interactions:
- Good: Under 200ms
- Needs Improvement: 200-500ms
- Poor: Over 500ms
CLS (Cumulative Layout Shift)
Visual stability:
- Good: Under 0.1
- Needs Improvement: 0.1-0.25
- Poor: Over 0.25
JavaScript Optimization
Code Splitting
// Instead of single large bundle
import { Chart } from './heavy-charts';
// Use dynamic imports
const Chart = lazy(() => import('./heavy-charts'));
function Dashboard() {
return (
<Suspense fallback={<ChartSkeleton />}>
<Chart data={data} />
</Suspense>
);
}
Route-Based Splitting
// vite.config.js or webpack config
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom'],
'vendor-charts': ['chart.js', 'react-chartjs-2'],
'vendor-utils': ['lodash', 'moment'],
},
},
},
},
};
Tree Shaking Verification
// This imports entire lodash (4MB+)
import _ from 'lodash';
_.pick(obj, ['a', 'b']);
// This imports only pick (4KB)
import pick from 'lodash/pick';
pick(obj, ['a', 'b']);
// Even better: use ESM-native utilities
import { pick } from 'lodash-es';
Image Optimization
Modern Formats
<!-- Before: PNG -->
<img src="hero.png" alt="Hero" width="1200" height="600" />
<!-- After: WebP/AVIF with fallbacks -->
<picture>
<source srcset="hero.avif" type="image/avif" />
<source srcset="hero.webp" type="image/webp" />
<img src="hero.jpg" alt="Hero" width="1200" height="600" loading="lazy" />
</picture>
Responsive Images
<img
srcset="hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
src="hero-800.jpg"
alt="Hero"
loading="eager"
/>
Lazy Loading Strategy
// Only load what's visible
function useIntersectionObserver(
ref: React.RefObject<Element>,
options?: IntersectionObserverInit
) {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => setIsVisible(entry.isIntersecting),
{ threshold: 0.1, ...options }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, [ref, options]);
return isVisible;
}
Caching Strategies
Browser Caching
// Service Worker for offline capability
// sw.js
const CACHE_NAME = 'app-cache-v1';
const ASSETS = ['/', '/index.html', '/styles.css', '/app.js'];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(fetchResponse => {
return caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
});
});
})
);
});
API Caching with Stale-While-Revalidate
// Server-side caching strategy
async function fetchWithCache(key: string, fetcher: () => Promise<any>) {
const cached = await redis.get(key);
if (cached) {
// Return cached immediately, update in background
setImmediate(() => {
fetcher().then(fresh => redis.setex(key, 300, JSON.stringify(fresh)));
});
return JSON.parse(cached);
}
const fresh = await fetcher();
redis.setex(key, 300, JSON.stringify(fresh));
return fresh;
}
CSS Optimization
Critical CSS
// Extract critical CSS
import { extractCritical } from '@emotion/react';
const { css, ids } = extractCritical(renderToString(<App />));
// Inline in HTML head
<html>
<head>
<style>{css}</style>
</head>
<body>
<div id="root" {...ids}></div>
</body>
</html>
Avoid Layout Thrashing
// Bad: Forces multiple reflows
const elements = document.querySelectorAll('.item');
for (const el of elements) {
el.style.width = '100px';
el.style.height = '100px';
document.body.appendChild(el);
}
// Good: Batch DOM reads and writes
const elements = document.querySelectorAll('.item');
// Read first
const widths = elements.map(el => el.offsetWidth);
// Write after
requestAnimationFrame(() => {
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = widths[i] + 'px';
}
});
Performance Budgets
Set and enforce performance budgets:
// lighthouse.config.js
module.exports = {
passes: [{
recordTrace: true,
performanceMetrics: {
lcp: 2500,
interactive: 3000,
cls: 0.1,
}
}],
assert: {
assertions: {
'first-contentful-paint': ['error', { maxNumericValue: 1500 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
}
}
};
What Students Should Do Next
- Measure first with Lighthouse or WebPageTest — Don’t guess where performance issues are
- Focus on JavaScript reduction — It usually has the biggest impact
- Set up performance budgets in CI/CD — Catch regressions before they reach production
Performance optimization is iterative. Measure, optimize, measure again. The numbers don’t lie.
Share this article
Ravali
Software Engineer & Content Creator
Ravali writes practical engineering guides for students and developers, combining hands-on project stories, career lessons, and trend-focused technical research.