Back to blog

Modern Web Performance Optimization - A Complete Guide

7 min readBy Mustafa Akkaya
#Performance#Web Vitals#Optimization#Next.js

In today's web landscape, performance isn't just a nice-to-have—it's essential. Users expect instant loading, smooth interactions, and Google rewards fast sites with better rankings. Let's dive into practical techniques to optimize your web applications.

Why Performance Matters

The impact of web performance is measurable:

- 1 second delay = 7% reduction in conversions

- 53% of users abandon sites that take longer than 3 seconds

- Google Search uses Core Web Vitals as a ranking factor

- Better UX = Higher engagement and retention

- Lower costs = Reduced server and bandwidth expenses

Core Web Vitals

Google's Core Web Vitals measure real-world user experience:

Largest Contentful Paint (LCP)

Measures loading performance. Should occur within 2.5 seconds.

script.js

class="keyword">class="comment">// Optimize LCP with Next.js Image

class="keyword">import Image class="keyword">from class="keyword">class="string">"next/image";

class="keyword">export class="keyword">default class="keyword">class="keyword">function Hero() {

class="keyword">class="keyword">return (

src=class="keyword">class="string">"/hero.jpg"

alt=class="keyword">class="string">"Hero image"

width={1200}

height={600}

priority class="keyword">class="comment">// Preload above-the-fold images

quality={85}

/>

);

}

First Input Delay (FID)

Measures interactivity. Should be less than 100 milliseconds.

script.js

class="keyword">class="comment">// Break up long tasks

class="keyword">class="keyword">async class="keyword">class="keyword">function processData(items) {

class="keyword">class="keyword">const chunks = chunkArray(items, 50);

class="keyword">class="keyword">for (class="keyword">class="keyword">const chunk of chunks) {

class="keyword">class="keyword">await class="keyword">new Promise((resolve) => {

requestIdleCallback(() => {

processChunk(chunk);

resolve();

});

});

}

}

Cumulative Layout Shift (CLS)

Measures visual stability. Should be less than 0.1.

styles.css

class="keyword">class="comment">/* Reserve space class="keyword">class="keyword">for images */

.image-container {

aspect-ratio: 16 / 9;

width: 100%;

}

class="keyword">class="comment">/* Prevent layout shift class="keyword">from web fonts */

@font-face {

font-family: class="keyword">class="string">"MyFont";

src: url(class="keyword">class="string">"/fonts/myfont.woff2") format(class="keyword">class="string">"woff2");

font-display: swap; class="keyword">class="comment">/* Use fallback until loaded */

}

Image Optimization

Images often account for 50%+ of page weight.

Modern Formats

script.js

class="keyword">class="comment">// Next.js automatically serves WebP/AVIF

src=class="keyword">class="string">"/photo.jpg"

alt=class="keyword">class="string">"Photo"

width={800}

height={600}

class="keyword">class="comment">// Next.js serves optimal format per browser

/>

Responsive Images

script.js

class="keyword">class="comment">// Serve different sizes per viewport

src=class="keyword">class="string">"/banner.jpg"

alt=class="keyword">class="string">"Banner"

fill

sizes=class="keyword">class="string">"(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"

/>

Lazy Loading

script.js

class="keyword">class="comment">// Lazy load images below the fold

src=class="keyword">class="string">"/product.jpg"

alt=class="keyword">class="string">"Product"

width={400}

height={400}

loading=class="keyword">class="string">"lazy" class="keyword">class="comment">// Native lazy loading

/>

Placeholder Blur

script.js

class="keyword">class="comment">// Show blur placeholder class="keyword">class="keyword">while loading

src=class="keyword">class="string">"/hero.jpg"

alt=class="keyword">class="string">"Hero"

width={1200}

height={600}

placeholder=class="keyword">class="string">"blur"

blurDataURL=class="keyword">class="string">"data:image/jpeg;base64,/9j/4AAQSkZJRg..." class="keyword">class="comment">// Generated base64

/>

Code Splitting

Split your JavaScript bundles for faster initial loads.

Dynamic Imports

script.js

class="keyword">import dynamic class="keyword">from class="keyword">class="string">"next/dynamic";

class="keyword">class="comment">// Load component only when needed

class="keyword">class="keyword">const HeavyComponent = dynamic(() => class="keyword">import(class="keyword">class="string">"./HeavyComponent"), {

loading: () => ,

ssr: false, class="keyword">class="comment">// Client-side only

});

class="keyword">export class="keyword">default class="keyword">class="keyword">function Page() {

class="keyword">class="keyword">const [show, setShow] = useState(false);

class="keyword">class="keyword">return (

{show && }

);

}

Route-based Splitting

script.js

class="keyword">class="comment">// Next.js automatically splits by route

class="keyword">class="comment">// Each page gets its own bundle

class="keyword">class="comment">// app/dashboard/page.tsx

class="keyword">export class="keyword">default class="keyword">class="keyword">function Dashboard() {

class="keyword">class="keyword">return class="keyword">class="comment">// Only loaded on /dashboard

}

class="keyword">class="comment">// app/settings/page.tsx

class="keyword">export class="keyword">default class="keyword">class="keyword">function Settings() {

class="keyword">class="keyword">return class="keyword">class="comment">// Only loaded on /settings

}

Caching Strategies

Leverage caching to minimize network requests.

Static Generation

script.js

class="keyword">class="comment">// Pre-render at build time

class="keyword">export class="keyword">class="keyword">async class="keyword">class="keyword">function generateStaticParams() {

class="keyword">class="keyword">const posts = class="keyword">class="keyword">await getPosts();

class="keyword">class="keyword">return posts.map((post) => ({ slug: post.slug }));

}

class="keyword">export class="keyword">default class="keyword">class="keyword">async class="keyword">class="keyword">function Post({ params }) {

class="keyword">class="keyword">const post = class="keyword">class="keyword">await getPost(params.slug);

class="keyword">class="keyword">return

;

}

Incremental Static Regeneration

script.js

class="keyword">class="comment">// Revalidate class="keyword">static pages periodically

class="keyword">export class="keyword">class="keyword">const revalidate = 60; class="keyword">class="comment">// Revalidate every 60 seconds

class="keyword">export class="keyword">default class="keyword">class="keyword">async class="keyword">class="keyword">function ProductPage({ params }) {

class="keyword">class="keyword">const product = class="keyword">class="keyword">await fetch(class="keyword">class="string">/api/products/${params.id}, {

next: { revalidate: 60 },

}).then((res) => res.json());

class="keyword">class="keyword">return ;

}

HTTP Caching

script.js

class="keyword">class="comment">// Set cache headers

class="keyword">export class="keyword">class="keyword">async class="keyword">class="keyword">function GET() {

class="keyword">class="keyword">const data = class="keyword">class="keyword">await fetchData();

class="keyword">class="keyword">return class="keyword">new Response(JSON.stringify(data), {

headers: {

class="keyword">class="string">"Cache-Control": class="keyword">class="string">"class="keyword">public, max-age=3600, stale-class="keyword">class="keyword">while-revalidate=86400",

class="keyword">class="string">"Content-Type": class="keyword">class="string">"application/json",

},

});

}

Font Optimization

Fonts can significantly impact performance.

Next.js Font Optimization

script.js

class="keyword">import { Inter, Roboto_Mono } class="keyword">from class="keyword">class="string">"next/font/google";

class="keyword">class="keyword">const inter = Inter({

subsets: [class="keyword">class="string">"latin"],

display: class="keyword">class="string">"swap",

variable: class="keyword">class="string">"--font-inter",

});

class="keyword">class="keyword">const robotoMono = Roboto_Mono({

subsets: [class="keyword">class="string">"latin"],

display: class="keyword">class="string">"swap",

variable: class="keyword">class="string">"--font-roboto-mono",

});

class="keyword">export class="keyword">default class="keyword">class="keyword">function RootLayout({ children }) {

class="keyword">class="keyword">return (

class="keyword">class="string">${inter.variable} ${robotoMono.variable}}> {children}

);

}

Subset Fonts

script.js

class="keyword">class="comment">// Only include characters you need

class="keyword">class="keyword">const notoSans = Noto_Sans({

subsets: [class="keyword">class="string">"latin"], class="keyword">class="comment">// Only Latin characters

weight: [class="keyword">class="string">"400", class="keyword">class="string">"700"], class="keyword">class="comment">// Only these weights

display: class="keyword">class="string">"swap",

});

Bundle Size Optimization

Keep your JavaScript bundles small.

Analyze Bundle

script.sh

# Next.js bundle analyzer

npm install @next/bundle-analyzer

# next.config.js

class="keyword">class="keyword">const withBundleAnalyzer = require(class="keyword">class="string">'@next/bundle-analyzer')({

enabled: process.env.ANALYZE === class="keyword">class="string">'true'

})

module.exports = withBundleAnalyzer({

class="keyword">class="comment">// your config

})

# Analyze

ANALYZE=true npm run build

Tree Shaking

script.js

class="keyword">class="comment">// Import only what you need

class="keyword">import { debounce } class="keyword">from class="keyword">class="string">"lodash-es"; class="keyword">class="comment">// ✅ Tree-shakeable

class="keyword">class="comment">// vs

class="keyword">import _ class="keyword">from class="keyword">class="string">"lodash"; class="keyword">class="comment">// ❌ Imports entire library

Remove Unused Dependencies

script.sh

# Find unused dependencies

npx depcheck

# Remove unused packages

npm uninstall unused-package

Compression

Compress assets for faster transfers.

Enable Compression

script.js

class="keyword">class="comment">// next.config.js

module.exports = {

compress: true, class="keyword">class="comment">// Enable gzip compression

class="keyword">class="comment">// Or use Brotli(better compression)

class="keyword">class="keyword">async headers() {

class="keyword">class="keyword">return [

{

source: class="keyword">class="string">"/:path*",

headers: [

{

key: class="keyword">class="string">"Content-Encoding",

value: class="keyword">class="string">"br",

},

],

},

];

},

};

Prefetching & Preloading

Load resources proactively.

Link Prefetching

script.js

class="keyword">import Link class="keyword">from class="keyword">class="string">"next/link";

class="keyword">class="comment">// Next.js automatically prefetches visible links

class="keyword">class="string">"/about" prefetch={true}>

About

;

Resource Preloading

script.js

class="keyword">class="comment">// app/layout.tsx

class="keyword">export class="keyword">default class="keyword">class="keyword">function RootLayout({ children }) {

class="keyword">class="keyword">return (

rel=class="keyword">class="string">"preload"

href=class="keyword">class="string">"/fonts/inter.woff2"

as=class="keyword">class="string">"font"

class="keyword">type=class="keyword">class="string">"font/woff2"

crossOrigin=class="keyword">class="string">"anonymous"

/>

{children}

);

}

Monitoring Performance

Track performance in production.

Web Vitals Tracking

script.js

class="keyword">class="comment">// app/layout.tsx

class="keyword">import { Analytics } class="keyword">from class="keyword">class="string">"@vercel/analytics/react";

class="keyword">import { SpeedInsights } class="keyword">from class="keyword">class="string">"@vercel/speed-insights/next";

class="keyword">export class="keyword">default class="keyword">class="keyword">function RootLayout({ children }) {

class="keyword">class="keyword">return (

{children}

);

}

Custom Metrics

script.js

class="keyword">class="string">"use client";

class="keyword">import { useReportWebVitals } class="keyword">from class="keyword">class="string">"next/web-vitals";

class="keyword">export class="keyword">class="keyword">function WebVitals() {

useReportWebVitals((metric) => {

class="keyword">class="comment">// Send to analytics

console.log(metric);

class="keyword">class="comment">// Example: Send to Google Analytics

class="keyword">class="keyword">if (window.gtag) {

window.gtag(class="keyword">class="string">"event", metric.name, {

value: Math.round(metric.value),

metric_id: metric.id,

metric_value: metric.value,

metric_delta: metric.delta,

});

}

});

class="keyword">class="keyword">return class="keyword">null;

}

Database Query Optimization

Slow database queries kill performance.

Use Database Indexes

query.sql

-- Create index class="keyword">class="keyword">for frequently queried columns

class="keyword">CREATE INDEX idx_users_email class="keyword">ON users(email);

class="keyword">CREATE INDEX idx_posts_user_id class="keyword">ON posts(user_id);

Connection Pooling

script.js

class="keyword">import { Pool } class="keyword">from class="keyword">class="string">"pg";

class="keyword">class="comment">// Reuse connections

class="keyword">class="keyword">const pool = class="keyword">new Pool({

max: 20,

idleTimeoutMillis: 30000,

connectionTimeoutMillis: 2000,

});

class="keyword">export class="keyword">class="keyword">async class="keyword">class="keyword">function getUser(id) {

class="keyword">class="keyword">const client = class="keyword">class="keyword">await pool.connect();

class="keyword">try {

class="keyword">class="keyword">const result = class="keyword">class="keyword">await client.query(class="keyword">class="string">"class="keyword">SELECT * class="keyword">FROM users class="keyword">WHERE id = $1", [

id,

]);

class="keyword">class="keyword">return result.rows[0];

} class="keyword">finally {

client.release();

}

}

Query Optimization

script.js

class="keyword">class="comment">// ❌ N+1 query problem

class="keyword">class="keyword">const users = class="keyword">class="keyword">await db.user.findMany();

class="keyword">class="keyword">for (class="keyword">class="keyword">const user of users) {

class="keyword">class="keyword">const posts = class="keyword">class="keyword">await db.post.findMany({ where: { userId: user.id } });

}

class="keyword">class="comment">// ✅ Single query with join

class="keyword">class="keyword">const users = class="keyword">class="keyword">await db.user.findMany({

include: {

posts: true,

},

});

Performance Checklist

Before deploying:

- ✅ Optimize images (WebP/AVIF, lazy loading)

- ✅ Enable code splitting and dynamic imports

- ✅ Use static generation where possible

- ✅ Implement caching strategies

- ✅ Optimize fonts (subset, preload)

- ✅ Minimize bundle size (tree shaking)

- ✅ Enable compression (Brotli/gzip)

- ✅ Prefetch critical resources

- ✅ Monitor Core Web Vitals

- ✅ Optimize database queries

- ✅ Use CDN for static assets

- ✅ Minify CSS/JS in production

Conclusion

Web performance optimization is an ongoing process. Start with Core Web Vitals, optimize images and fonts, implement smart caching, and continuously monitor your metrics.

Remember: fast websites win. They rank better, convert more, and provide superior user experiences. Every millisecond counts.

Happy optimizing! ⚡