Preloading your primary font eliminates the flash of invisible text (FOIT) and reduces LCP when text is the largest element.
index.html
<head>
<!-- Preload the critical font file -->
<link rel="preload"
href="/fonts/Inter-Variable.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous" />
<style>
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Variable.woff2') format('woff2');
font-display: swap; "color:#6a737d;font-style:italic">/* or 'optional'for zero CLS */
font-weight: 100900;
unicode-range: U+0000-00FF; "color:#6a737d;font-style:italic">/* Latin subset */
}
</style>
</head>
CLS
Always Set Image Width & Height
The browser reserves space before images load, preventing layout shifts. Use aspect-ratio as the modern CSS approach.
styles.css
"color:#6a737d;font-style:italic">/* Modern CSS approach — set aspect ratio on containers */
.image-container {
aspect-ratio: 16 / 9;
width: 100%;
overflow: hidden;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
"color:#6a737d;font-style:italic">/* Or use width/height attributes directly on <img> */
"color:#6a737d;font-style:italic">/* <img src="photo.jpg" width="800" height="450" alt="..."> */
"color:#6a737d;font-style:italic">/* For dynamic iframes or embeds */
.embed-container {
position: relative;
padding-bottom: 56.25%; "color:#6a737d;font-style:italic">/* 16:9 */
height: 0;
}
.embed-container iframe {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
}
CLS
Prevent Font-Swap Layout Shifts
Use the size-adjust and ascent-override CSS descriptors to make fallback fonts match your web font dimensions.
fonts.css
"color:#6a737d;font-style:italic">/* Define a size-adjusted fallback to match your custom font */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
size-adjust: 107%;
}
"color:#6a737d;font-style:italic">/* Use the fallback in your font stack */
body {
font-family: 'Inter', 'Inter Fallback', system-ui, sans-serif;
}
"color:#6a737d;font-style:italic">/* In Next.js — use the built-in font optimizer */
"color:#6a737d;font-style:italic">/* import { Inter } from'next/font/google'const inter = Inter({
subsets: ['latin'],
display: 'swap', "color:#6a737d;font-style:italic">// 'optional'for zero CLS
preload: true,
}) */
CLS
Reserve Space for Dynamic Content
Ads, banners, and late-loading content cause massive CLS. Always reserve space with min-height or skeleton placeholders.
Long JavaScript tasks (>50ms) block user interaction. Yield to the browser periodically to keep INP under 200ms.
tasks.js
"color:#6a737d;font-style:italic">// Yield to main thread between work chunks
functionyieldToMain() {
returnnewPromise(resolve => setTimeout(resolve, 0))
}
"color:#6a737d;font-style:italic">// Process a large array in chunks
asyncfunctionprocessLargeArray(items) {
const CHUNK_SIZE = 50const results = []
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE)
"color:#6a737d;font-style:italic">// Process chunk synchronously
for (const item of chunk) {
results.push(expensiveOperation(item))
}
"color:#6a737d;font-style:italic">// Yield to browser after each chunk
"color:#6a737d;font-style:italic">// This allows interaction events to be processed
if (i + CHUNK_SIZE < items.length) {
awaityieldToMain()
}
}
return results
}
"color:#6a737d;font-style:italic">// Modern scheduler API(Chrome 115+)
asyncfunctionprocessWithScheduler(items) {
for (const item of items) {
await scheduler.yield() "color:#6a737d;font-style:italic">// Yields if a user interaction is pending
processItem(item)
}
}
INP
Offload to Web Workers
Move CPU-intensive work off the main thread entirely. Web Workers run in a separate thread and never block user interactions.
worker-setup.js
"color:#6a737d;font-style:italic">// heavy-worker.js — runs in background thread
self.onmessage = function(e) {
const { data, operation } = e.data
let result
switch(operation) {
case 'sort':
result = data.sort((a, b) => a.score - b.score)
break
case 'filter':
result = data.filter(item => item.active)
break
case 'transform':
result = data.map(item => ({
...item,
computed: expensiveCalculation(item),
}))
break
}
self.postMessage({ result })
}
"color:#6a737d;font-style:italic">// main.js — stays responsive
const worker = newWorker('/heavy-worker.js')
functionprocessDataAsync(data, operation) {
returnnewPromise((resolve) => {
worker.onmessage = (e) => resolve(e.data.result)
worker.postMessage({ data, operation })
})
}
"color:#6a737d;font-style:italic">// Usage: main thread stays free for interactions
button.addEventListener('click', async () => {
button.textContent = 'Processing...'const sorted = awaitprocessDataAsync(largeDataset, 'sort')
renderTable(sorted)
button.textContent = 'Done!'
})
INP
Event Delegation Pattern
Instead of attaching listeners to every list item, delegate to a parent. Reduces memory usage and speeds up initial paint.
delegation.js
"color:#6a737d;font-style:italic">// ✗ BAD — one listener per item(slow with 1000+ items)
document.querySelectorAll('.list-item').forEach(item => {
item.addEventListener('click', handleClick)
})
"color:#6a737d;font-style:italic">// ✓ GOOD — one listener on parent, delegate to children
document.querySelector('.list-container')
.addEventListener('click', (e) => {
const item = e.target.closest('[data-item-id]')
if (!item) returnconst id = item.dataset.itemId
handleItemClick(id)
})
"color:#6a737d;font-style:italic">// Works for dynamically added items too!
"color:#6a737d;font-style:italic">// No need to re-attach listeners when list updates.
"color:#6a737d;font-style:italic">// React equivalent — single handler on parent
functionItemList({ items, onSelect }) {
return (
<ul onClick={(e) => {
const id = e.target.closest('[data-id]')?.dataset.id
if (id) onSelect(id)
}}>
{items.map(item => (
<li key={item.id} data-id={item.id}>{item.name}</li>
))}
</ul>
)
}
INP
Batch DOM Updates with rAF
Interleaving DOM reads and writes causes layout thrashing. Batch all reads first, then writes inside requestAnimationFrame.
dom-batching.js
"color:#6a737d;font-style:italic">// ✗ BAD — layout thrashing(read-write-read-write)
elements.forEach(el => {
const height = el.offsetHeight "color:#6a737d;font-style:italic">// FORCED LAYOUT(read)
el.style.height = height * 2 + 'px'"color:#6a737d;font-style:italic">// Write
"color:#6a737d;font-style:italic">// Next iteration forces layout again!
})
"color:#6a737d;font-style:italic">// ✓ GOOD — batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight) "color:#6a737d;font-style:italic">// Batch READ
requestAnimationFrame(() => {
elements.forEach((el, i) => {
el.style.height = heights[i] * 2 + 'px'"color:#6a737d;font-style:italic">// Batch WRITE
})
})
"color:#6a737d;font-style:italic">// For complex updates — use a read/write queue
const domScheduler = {
reads: [],
writes: [],
schedule() {
"color:#6a737d;font-style:italic">// Execute all reads first
this.reads.forEach(fn => fn())
"color:#6a737d;font-style:italic">// Then all writes in a single frame
requestAnimationFrame(() => {
this.writes.forEach(fn => fn())
this.reads = []
this.writes = []
})
}
}
Lazy Loading
Native Lazy Loading + Intersection Observer
Defer off-screen images and components using the native loading="lazy" attribute combined with Intersection Observer for JS-heavy components.
Split your app at the route level so users only download JS for the page they visit. This is automatic in Next.js, but here is how to do it in React Router.
routes.tsx
import { lazy, Suspense } from'react'import { BrowserRouter, Routes, Route } from'react-router-dom'"color:#6a737d;font-style:italic">// Lazy-load each route — only downloads when user navigates
const Home = lazy(() => import('./pages/Home'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
const HeavyReport = lazy(() =>
import("color:#6a737d;font-style:italic">/* webpackChunkName: "report" */ './pages/HeavyReport')
)
functionApp() {
return (
<BrowserRouter>
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/report" element={<HeavyReport />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}
"color:#6a737d;font-style:italic">// Next.js does this automatically for every file in /app
"color:#6a737d;font-style:italic">// Each page.tsx is its own code-split chunk
Lazy Loading
Lazy <picture> with Fallback
Combine lazy loading with the <picture> element for format negotiation. Include a low-quality placeholder for the best perceived performance.
Stream HTML to the browser as it renders instead of waiting for the full page. Users see content faster and TTFB drops to near-zero for the first chunk.
page.tsx
"color:#6a737d;font-style:italic">// Next.js App Router — automatic streaming with Suspense
import { Suspense } from'react'"color:#6a737d;font-style:italic">// This component fetches data — server streams it when ready
asyncfunctionProductList() {
const products = awaitfetch('https://api.store.com/products')
.then(r => r.json())
return (
<div className="grid">
{products.map(p => (
<ProductCard key={p.id} product={p} />
))}
</div>
)
}
"color:#6a737d;font-style:italic">// Page component — shell is sent instantly
exportdefaultfunctionStorePage() {
return (
<main>
{"color:#6a737d;font-style:italic">/* This renders immediately in the first HTML chunk */}
<h1>Our Products</h1>
<p>Browse our latest collection</p>
{"color:#6a737d;font-style:italic">/* This streams in when data is ready */}
<Suspense fallback={<ProductGridSkeleton />}>
<ProductList />
</Suspense>
</main>
)
}
"color:#6a737d;font-style:italic">// The browser receives the <h1> and skeleton instantly,
"color:#6a737d;font-style:italic">// then the product list streams in once the API responds.
"color:#6a737d;font-style:italic">// TTFB for the first byte is effectively 0ms after edge cache.
Security
Content Security Policy Headers
CSP prevents XSS attacks and controls which resources browsers can load. A misconfigured third-party script cannot exfiltrate data with a strict CSP.