Frontend Performance Optimization in 2023
Frontend performance affects user experience directly. Slow loading times increase bounce rates. Laggy interactions frustrate users. Even small delays impact conversion rates.
The web platform evolves and so do optimization techniques. What worked in 2020 might not be the best approach in 2023.
Core Web Vitals
Google's Core Web Vitals measure user experience:
Largest Contentful Paint (LCP): How quickly the main content loads. Target under 2.5 seconds.
First Input Delay (FID): Measures responsiveness to user input. Target under 100ms.
Cumulative Layout Shift (CLS): Visual stability. Elements shouldnt jump around during load. Target under 0.1.
These metrics correlate with real user experience and affect search rankings.
Image optimization
Images are often the largest assets. Optimize them:
Use modern formats like WebP or AVIF:
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description" loading="lazy">
</picture>
Browsers automatically choose the best supported format.
Size images appropriately:
<img
src="hero-1200.jpg"
srcset="
hero-600.jpg 600w,
hero-1200.jpg 1200w,
hero-2400.jpg 2400w
"
sizes="(max-width: 600px) 600px, (max-width: 1200px) 1200px, 2400px"
alt="Hero image"
>
Browsers load the right size for the viewport. Don't send 4K images to mobile devices.
Code splitting
Don't ship all JavaScript upfront. Split by route:
// React with lazy loading
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Users only download code for routes they visit.
Split large libraries:
// Bad - imports entire library
import { debounce } from 'lodash';
// Better - imports only what you need
import debounce from 'lodash/debounce';
Or use libraries designed for tree-shaking like lodash-es.
Preloading and prefetching
Tell browsers what to load early:
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/critical.css" as="style">
<!-- Prefetch for likely navigation -->
<link rel="prefetch" href="/dashboard.js">
Preload loads resources immediately. Prefetch loads when browser is idle.
For Next.js Link components automatically prefetch:
<Link href="/dashboard" prefetch={true}>
Dashboard
</Link>
Font loading
Web fonts can cause layout shift or invisible text. Use font-display:
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap;
}
font-display: swap shows fallback font immediately then swaps when custom font loads.
Preload critical fonts:
<link
rel="preload"
href="/fonts/main.woff2"
as="font"
type="font/woff2"
crossorigin
>
JavaScript execution
Large JavaScript bundles block the main thread. Optimize:
Defer non-critical scripts:
<script src="analytics.js" defer></script>
Or use async for scripts that dont depend on DOM:
<script src="tracking.js" async></script>
Break up long tasks:
// Bad - blocks main thread
function processLargeArray(items) {
items.forEach(item => {
expensiveOperation(item);
});
}
// Better - yields to browser
async function processLargeArray(items) {
for (const item of items) {
expensiveOperation(item);
// Yield every 50 items
if (items.indexOf(item) % 50 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
This lets the browser handle user input between chunks.
Third-party scripts
Third-party scripts often hurt performance. Audit what you load:
// Measure third-party impact
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'script') {
console.log(entry.name, entry.duration);
}
}
});
observer.observe({ entryTypes: ['resource'] });
Load third-party scripts after main content:
window.addEventListener('load', () => {
// Load analytics after page loads
const script = document.createElement('script');
script.src = 'https://analytics.example.com/script.js';
document.body.appendChild(script);
});
Caching strategies
Cache assets aggressively:
# .htaccess or server config
<FilesMatch "\.(jpg|jpeg|png|gif|svg|woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "\.(js|css)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
Use content hashes in filenames so cache breaks when files change:
app-a3f2b1c.js
styles-d4e5f6g.css
Service workers cache dynamically:
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/app.js',
'/styles.css',
]);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
CSS optimization
Remove unused CSS with PurgeCSS or similar:
// postcss.config.js
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.html', './src/**/*.jsx'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
})
]
}
This removes CSS not referenced in your templates.
Use CSS containment for complex layouts:
.widget {
contain: layout style paint;
}
This tells browsers the widget wont affect outside layout improving rendering performance.
Monitoring performance
Use Real User Monitoring (RUM) to track actual user experience:
// web-vitals library
import { getCLS, getFID, getLCP } from 'web-vitals';
function sendToAnalytics({ name, value, id }) {
fetch('/analytics', {
method: 'POST',
body: JSON.stringify({ metric: name, value, id }),
});
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
This shows how real users experience your site not just synthetic tests.
Database and API optimization
Frontend performance depends on backend speed. Optimize API calls:
// Bad - sequential requests
const user = await fetch('/api/user');
const posts = await fetch('/api/posts');
const comments = await fetch('/api/comments');
// Better - parallel requests
const [user, posts, comments] = await Promise.all([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/comments'),
]);
Cache API responses:
const cache = new Map();
async function fetchWithCache(url) {
if (cache.has(url)) {
return cache.get(url);
}
const response = await fetch(url);
const data = await response.json();
cache.set(url, data);
return data;
}
Build optimization
Modern build tools handle many optimizations automatically. Verify your setup:
// vite.config.js
export default {
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove console.logs
},
},
rollupOptions: {
output: {
manualChunks: {
'vendor': ['react', 'react-dom'],
},
},
},
},
};
This splits vendor code from application code for better caching.
Progressive enhancement
Build pages that work without JavaScript then enhance:
<!-- Works without JS -->
<form action="/search" method="GET">
<input name="q" type="search">
<button>Search</button>
</form>
<script>
// Enhance with JS
document.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault();
const results = await searchAPI(e.target.q.value);
displayResults(results);
});
</script>
Core functionality works immediately while JavaScript loads. Users dont have to wait for the full app bundle.
Common mistakes
Premature optimization. Measure first then optimize bottlenecks.
Over-optimizing everything. Some pages dont need aggressive optimization if they're rarely visited.
Ignoring mobile. Test performance on actual mobile devices not just desktop Chrome. Mobile networks and CPUs are much slower.
Not measuring real users. Lab metrics dont always match field performance.
Summary
Frontend performance optimization in 2023 focuses on Core Web Vitals, code splitting, and modern image formats. Measure real user experience with RUM data.
Start with the biggest wins: optimize images, split code by route, and lazy load non-critical resources. Use modern build tools that handle many optimizations automatically. The web platform provides more performance APIs and better defaults than ever before.