Skip to main content

Command Palette

Search for a command to run...

Web Vitals in Frontend.

Published
11 min read
A

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:

  1. Downloads HTML.

  2. Sees JavaScript files.

  3. Stops rendering.

  4. Downloads and runs that JavaScript.

  5. 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:

  1. Stops showing the page.

  2. Downloads CSS.

  3. Applies styles.

  4. 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 defer for scripts

  • Load 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

TermSimple MeaningHow it reduces blocking
Render-blockingFiles that stop page displayLoad later / shrink
Code splittingBreak JS into chunksSmaller first load
Lazy loadingLoad only when neededLess startup work
HydrationJS makes HTML interactiveReduce JS cost
StreamingSend HTML earlyContent 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.lazy or 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:

  1. Images without width/height

  2. Ads or banners injected late

  3. Fonts swapping after render

  4. Client-only components appearing later

  5. Skeleton missing space

  6. 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:

  1. Long-running JavaScript tasks

  2. Large bundles executing on load

  3. Expensive React re-renders

  4. Heavy event handlers

  5. Layout thrashing

  6. Third-party scripts

  7. 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

MetricFocusGood Score
LCPLoading speed≤ 2.5s
CLSVisual stability≤ 0.1
INPInteractivity≤ 200ms