Web Vitals in Frontend.
Full Stack Developer specializing in sleek, performant frontends and scalable backend systems I build production ready web applications with a focus on scalability, performance, and clean architecture. My expertise spans modern frontend development with React.js , Next.js and TypeScript, combined with robust backend systems.
On the backend, I specialize in Golang microservices using Fiber framework, implementing event-driven architectures with Kafka, caching strategies with Redis, and building efficient APIs with gRPC. I focus on creating scalable, maintainable systems that handle real-world complexity.
Web Vitals are a set of performance metrics defined by Google to measure real-world user experience on websites. In frontend development, they guide how fast, stable, and responsive a page feels.
Core Web Vitals (most important)
1. LCP – Largest Contentful Paint
What it measures:
How long it takes for the main visible content (hero image, large text block, etc.) to load.
Good: ≤ 2.5s
Frontend improvements:
Optimize images (compression, modern formats like WebP/AVIF)
Preload hero images/fonts
Reduce render-blocking CSS/JS
Use CDN
Server-side rendering / streaming
React & Next.js Optimization
1. LCP – Largest Contentful Paint
Common causes:
Large hero images
Slow SSR/TTFB
Blocking JS/CSS
Web fonts
Fixes:
Images
- Use Next.js
<Image />with priority:
<Image src="/hero.jpg" fill priority alt="hero" />
Serve AVIF/WebP
Preload critical images
Streaming / SSR
Use Next.js App Router streaming
Cache server components
Enable edge rendering
Fonts
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });
Reduce blocking
- Dynamic imports:
const Chart = dynamic(() => import('./Chart'), { ssr: false });
The phrase “Enable edge rendering” refers to running part of your web app on servers that are physically closer to users instead of only on a single central server.
What “edge rendering” means
Normally:
A user opens your site.
Their browser sends a request to your main server (maybe in one country).
The server generates the HTML.
The HTML is sent back.
The browser starts showing the page.
This travel time adds delay.
With edge rendering:
Your app is deployed to many small servers around the world (called edge locations).
The user’s request goes to the closest one.
That nearby server generates or serves the page.
The page arrives faster.
Result: the first visible content loads sooner → improves LCP.
Load some JavaScript later instead of forcing the browser to load everything at the start.
Below is a very simple, step-by-step explanation.
What is “blocking” here?
When a page loads, the browser:
Downloads HTML.
Sees JavaScript files.
Stops rendering.
Downloads and runs that JavaScript.
Then continues showing the page.
If the JavaScript is big or slow, the page waits.
That waiting time is called blocking.
The user stares at a blank or half-loaded page.
What dynamic imports do
Normally (static import):
import Chart from './Chart';
This means:
Load Chart immediately when the page loads — even if the user hasn’t used it yet.
So:
Browser downloads that code
Executes it
Page waits → blocking
Dynamic import:
const Chart = dynamic(() => import('./Chart'));
or in plain JS:
import('./Chart.js');
This means:
Don’t load Chart at page load.
Load it only when it’s actually needed.
How this reduces blocking
Instead of:
❌ Loading everything upfront
→ big JS bundle
→ browser stuck executing
→ slow page
You get:
✅ Load only the important code first
→ page becomes visible faster
→ user can interact
→ extra features load later in background
Real-world analogy
Imagine moving into a house.
Bad way:
Bring all furniture on day one.
Hallway blocked.
You can’t enter quickly.
Dynamic import:
Bring sofa and bed first.
Start living.
Bring bookshelf later when needed.
Why this helps Web Vitals
Dynamic imports improve:
✅ LCP
Main content appears sooner.
✅ INP
Less JavaScript running at start → interactions respond faster.
✅ TBT (lab metric)
Shorter blocking tasks.
One-line summary
Dynamic imports reduce blocking by shrinking the amount of JavaScript that must run before the page can show and respond.
Below is a explanation of each item from that list, focused on what it is and how it affects blocking during page load.
1) Render-Blocking Resources
What it means
A render-blocking resource is a file the browser must finish loading before it can show the page.
Usually:
CSS files
Some JavaScript files
How blocking happens
When the browser reads HTML and sees:
<link rel="stylesheet" href="styles.css">
It thinks:
“I can’t paint anything until I get this CSS.”
So it:
Stops showing the page.
Downloads CSS.
Applies styles.
Then renders.
If that file is slow → page stays blank longer.
Same with JavaScript:
<script src="main.js"></script>
Browser:
Stops parsing HTML
Downloads script
Runs it
Only then continues
How we reduce this blocking
Make CSS smaller
Inline critical CSS
Use
deferfor scriptsLoad non-critical styles later
2) Code Splitting
Code splitting = breaking one big JavaScript bundle into smaller pieces.
Instead of:
❌ One huge file for the whole app
You make:
✅ One small file for the homepage
✅ Another for admin panel
✅ Another for charts
How it helps
Smaller initial file =
Less download time
Less execution time
Browser unblocks sooner
Page appears faster
3) Lazy Loading
What it means
Lazy loading = load things only when needed.
Examples:
Images below the screen
Videos
Modals
Map widgets
How it helps
If something is not visible yet:
Why load it now?
By delaying it:
Main page loads faster
Browser does less work
User can interact sooner
4) Hydration
In React / Next.js:
Server sends ready-made HTML.
User sees the page.
Browser downloads JS.
JS “wakes up” the page so buttons work.
That “waking up” step is called hydration.
How hydration can block
If hydration JS is big:
Browser is busy running it.
Clicks feel slow.
Inputs lag.
That hurts INP.
How to reduce hydration blocking
Ship less JS
Use Server Components
Partial hydration / islands
Split interactive parts
5) Streaming
What it means
Streaming = send HTML in pieces instead of waiting for the whole page to be ready.
Normal way:
Server builds entire page.
Sends it.
Browser waits.
Then shows everything.
Streaming:
Server sends top part first.
Browser renders immediately.
Rest arrives later.
How it helps blocking
User sees content sooner:
Improves LCP
Reduces blank screen time
Feels faster
Quick Summary
| Term | Simple Meaning | How it reduces blocking |
| Render-blocking | Files that stop page display | Load later / shrink |
| Code splitting | Break JS into chunks | Smaller first load |
| Lazy loading | Load only when needed | Less startup work |
| Hydration | JS makes HTML interactive | Reduce JS cost |
| Streaming | Send HTML early | Content appears sooner |
Code splitting is phenomena to split the code in smaller bundle and lazy loading will load that split code or bundle according to requirement.
Code splitting can be used alone, but that usually just creates multiple bundles without real user-visible benefit.
In React, best practice is to combine code splitting with lazy loading (via
React.lazyor framework routing) so unused code is not downloaded until needed and performance actually improves.
2. CLS – Cumulative Layout Shift
What it measures:
How much the page layout unexpectedly moves while loading.
Good: ≤ 0.1
What is CLS?
CLS measures how much the page layout unexpectedly moves while loading.
Example bad experience:
You try to click a button.
An image loads above it.
The button jumps.
You mis-click.
That jump = layout shift → hurts CLS.
What causes CLS?
Most common frontend causes:
Images without width/height
Ads or banners injected late
Fonts swapping after render
Client-only components appearing later
Skeleton missing space
Dynamic content above existing content
React Example — BAD CLS
function Page() {
return (
<div>
<img src="/hero.jpg" />
<h1>Welcome</h1>
</div>
);
}
Problem:
Browser doesn’t know image size → renders text → image loads → pushes text down.
React Fix — GOOD CLS
function Page() {
return (
<div>
<img
src="/hero.jpg"
width="800"
height="400"
/>
<h1>Welcome</h1>
</div>
);
}
Now browser reserves space → no jump.
Font CLS Example
BAD:
@font-face {
font-family: MyFont;
src: url("/font.woff2");
}
Text invisible → font loads → text changes size → shift.
GOOD:
@font-face {
font-family: MyFont;
src: url("/font.woff2");
font-display: swap;
}
How Next.js Helps CLS
Next.js adds tooling that prevents common CLS problems.
1) next/image reserves space
import Image from "next/image";
<Image
src="/hero.jpg"
width={800}
height={400}
alt="hero"
/>
Next.js:
• calculates aspect ratio
• reserves layout space
• avoids shift
2) Next.js Fonts
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
});
This prevents invisible text + large font shifts.
3) App Router + Server Components
Server Components render HTML on the server:
• user sees stable layout earlier
• fewer client-only insertions
• lower CLS
Skeleton Loader (Correct Way)
BAD:
{loading && <Spinner />}
{!loading && <Card />}
Spinner has different height → jump.
GOOD:
<div style={{ height: 200 }}>
{loading ? <Skeleton /> : <Card />}
</div>
Space is reserved.
CLS Quick Rules
✔ Always size images / video
✔ Reserve ad slots
✔ Use skeletons with same height
✔ Use next/image
✔ Use next/font
✔ Avoid inserting banners above content
✔ Don’t shift content on hydration
One-sentence summary
CLS is caused by layout jumps during load; in React you fix it by reserving space, and in Next.js you mainly rely on
next/image, font optimization, and server rendering to keep layouts stable.
3. INP – Interaction to Next Paint (replaced FID in 2024)
What it measures:
How quickly the UI responds after a user interaction (click, tap, key press).
Good: ≤ 200ms
Below is a clear but thorough explanation of INP (Interaction to Next Paint), in the same style as the CLS explanation.
What is INP?
INP measures how fast the page visually responds after a user interaction (click, tap, key press).
It tracks:
• how long your JavaScript runs
• how long the browser waits
• when the screen actually updates
If the UI reacts slowly → INP is bad.
What causes poor INP?
Main frontend causes:
Long-running JavaScript tasks
Large bundles executing on load
Expensive React re-renders
Heavy event handlers
Layout thrashing
Third-party scripts
Main thread blocked
React Example — BAD INP
function Button() {
const handleClick = () => {
// heavy work blocks UI
for (let i = 0; i < 1e9; i++) {}
setOpen(true);
};
return <button onClick={handleClick}>Open</button>;
}
The loop blocks the main thread → click feels frozen.
React Fix — Split the work
function Button() {
const handleClick = () => {
setOpen(true); // update UI first
setTimeout(() => {
heavyCalculation();
}, 0);
};
return <button onClick={handleClick}>Open</button>;
}
UI updates quickly → heavy work runs after.
React Optimization Patterns
Memoization
const Card = React.memo(CardComponent);
Prevents unnecessary re-renders.
Debounce input
const onChange = debounce(handleSearch, 300);
Avoid running logic on every keystroke.
How Next.js Helps INP
1) Smaller JS via Server Components
By default in App Router:
• components run on server
• no JS sent to browser
Less JS → faster interactions.
2) Dynamic imports for heavy widgets
import dynamic from "next/dynamic";
const Chart = dynamic(() => import("./Chart"), {
ssr: false,
});
Heavy code loads only when needed → reduces blocking.
3) Streaming + partial hydration
Interactive parts hydrate separately → user can interact sooner.
Web Worker for heavy work
const worker = new Worker("/worker.js");
worker.postMessage(data);
Moves CPU work off main thread.
INP Quick Rules
✔ Keep JS small
✔ Avoid long tasks
✔ Memoize components
✔ Split heavy features
✔ Defer third-party scripts
✔ Use Server Components
✔ Web Workers for CPU work
One-sentence summary
INP measures how quickly the UI updates after interaction; in React you improve it by reducing main-thread work, and in Next.js mainly by shipping less client JS and splitting heavy features.
Other Web Vitals (supporting)
TTFB – Time to First Byte (server/network speed)
FCP – First Contentful Paint
TBT – Total Blocking Time (lab metric related to INP)
How to Measure in Frontend
Tools:
Chrome DevTools → Performance / Lighthouse
PageSpeed Insights
Search Console → Core Web Vitals
Web-Vitals JS library
Example:
import { onLCP, onCLS, onINP } from 'web-vitals';
onLCP(console.log);
onCLS(console.log);
onINP(console.log);
Why Web Vitals Matter
Direct ranking signal in Google Search
Better UX → higher conversion rates
Detect frontend bottlenecks early
Standard benchmark for performance budgets
Summary
| Metric | Focus | Good Score |
| LCP | Loading speed | ≤ 2.5s |
| CLS | Visual stability | ≤ 0.1 |
| INP | Interactivity | ≤ 200ms |


