এই পর্বে যা শিখবেন
- Core Web Vitals-এর আজকের তিনটি মাপ, তাদের সীমা আর 75th percentile-এর নিয়ম, আর lab ও field data-র পার্থক্য
- Production build-এর ওপর Edge-এ Lighthouse CLI চালানো, মোবাইল throttling বোঝা আর মূল audit পড়া;
web-vitalsদিয়ে নিজের endpoint-এ field data জমানো - LCP, CLS আর INP আলাদা করে মাপা আর সারানো: ছবি, preload,
fetchpriority,next/image,next/font, জায়গা রাখা, long task ভাঙা, third-party script - Cache-Control, ETag আর 304,
stale-while-revalidate,s-maxage, reverse proxy-র header আর gzip, Brotli, zstd-এর আসল আকার - ধীর নেটওয়ার্ক আর কম দামের ফোনের কথা মাথায় রেখে performance budget আর Lighthouse CI-র assertion
১ · দ্রুত মানে কী — আজকের Core Web Vitals
“সাইটটা ধীর” কথাটা খুব ঝাপসা। কার কাছে ধীর? কোন ফোনে? কোন মুহূর্তে? ঢাকার মোড়ের চায়ের দোকানের কথা ভাবুন। ক্রেতা তিনটি জিনিস খেয়াল করেন: চা কত তাড়াতাড়ি হাতে এল, “আরেকটা বিস্কুট দিন” বললে দোকানি কত তাড়াতাড়ি সাড়া দিলেন, আর বেঞ্চে বসার পর কেউ হঠাৎ ধাক্কা দিয়ে সরিয়ে দিল কি না। ওয়েবের পাতাতেও ঠিক এই তিনটি প্রশ্ন। Google-এর Chrome দল এগুলোকে তিনটি সংখ্যায় বেঁধেছে, নাম Core Web Vitals.
LCP · চা হাতে এল
Largest Contentful Paint: পাতার সবচেয়ে বড় দৃশ্যমান ছবি বা লেখার খণ্ড কখন আঁকা হলো। ভালো ≤ ২.৫ সেকেন্ড, ২.৫ থেকে ৪ সেকেন্ড উন্নতি দরকার, ৪ সেকেন্ডের বেশি খারাপ।
লোড হওয়াINP · দোকানির সাড়া
Interaction to Next Paint: ক্লিক, ট্যাপ বা কী চাপার পর পরের ছবি আঁকা পর্যন্ত সময়, পুরো visit-এর প্রায় সবচেয়ে খারাপটি। ভালো ≤ ২০০ ms, ৫০০ ms পর্যন্ত উন্নতি দরকার, তার বেশি খারাপ।
সাড়া দেওয়াCLS · বেঞ্চে ধাক্কা
Cumulative Layout Shift: হঠাৎ সরে যাওয়া element-এর মোট score, সবচেয়ে খারাপ “দল” ধরে। ভালো ≤ ০.১, ০.২৫ পর্যন্ত উন্নতি দরকার, তার বেশি খারাপ। এর কোনো একক নেই।
স্থির থাকাসীমাগুলো web.dev-এর বর্তমান লেখা থেকে মিলিয়ে নেওয়া (লেখার সময়ে, নভেম্বর ২০২৬)। দুটি নিয়ম মনে রাখুন। প্রথমত, একটি পাতা “ভালো” তখনই, যখন আসল page view-গুলোর 75th percentile তিনটি মাপেই সীমার ভেতরে থাকে, মোবাইল আর desktop আলাদা করে। অর্থাৎ চারজন ক্রেতার তিনজনের অভিজ্ঞতা ভালো হতে হবে; গড় নয়, কারণ গড় কয়েকজন খুব ধীর ক্রেতাকে লুকিয়ে ফেলে। দ্বিতীয়ত, এই তিনটির পাশে আরও দুটি সহায়ক মাপ আছে, যেগুলো আপনি আগেই চেনেন: TTFB (server-এর প্রথম byte, পর্ব ৫৫-এ মাপা) আর FCP (প্রথম কিছু আঁকা)। এগুলো রায়ের অংশ নয়, কিন্তু LCP কেন ধীর তা খুঁজতে কাজে লাগে।
Google ২০২০ সালে Core Web Vitals নামটি আনে, তখন তিনটি ছিল LCP, FID আর CLS. FID (First Input Delay) শুধু প্রথম ক্লিকের অপেক্ষা মাপত, handler চলার সময় আর পরের আঁকা নয়। তাই অনেক সাইট FID-এ ভালো থেকেও ক্রেতার কাছে আটকে যাওয়া মনে হতো। ২০২৩ সালে INP পরীক্ষামূলক অবস্থা থেকে “pending” হয়, আর ২০২৪ সালে (মার্চে) স্থায়ী Core Web Vital হয়ে FID-এর জায়গা নেয়। CLS-এর হিসাবও ২০২১ সালের দিকে বদলে “session window”-ভিত্তিক হয় (অংশ ৫)। Search-এ এগুলোর ভূমিকা কতটা, সেটি পর্ব ৫৮-এ দেখেছেন: একটি সংকেত, একমাত্র নয়।
Lab data আর field data — দুই রকম মাপ
Lab data মানে আপনি একটি নির্দিষ্ট যন্ত্রে, নির্দিষ্ট নেটওয়ার্ক ধরে, নিজে মাপছেন। সুবিধা: বারবার একই অবস্থায় চালানো যায়, আর একটি পরিবর্তনের আগে-পরে তুলনা করা যায়। অসুবিধা: আপনার ল্যাপটপ আপনার ক্রেতা নন। Field data মানে আসল ক্রেতার browser থেকে আসা মান: রাজশাহীর কেউ পুরোনো Android ফোনে, সিলেটের কেউ নতুন iPhone-এ, কেউ লিফটে দুর্বল সিগন্যালে। রায় হয় field data দিয়ে; কিন্তু “কেন” খুঁজতে লাগে lab. আরেকটি পার্থক্য গুরুত্বপূর্ণ: lab-এ সাধারণত কেউ ক্লিক করে না, তাই Lighthouse INP মাপে না। তার বদলে দেয় TBT (Total Blocking Time), যা বলে পাতা লোডের সময় main thread কতক্ষণ আটকে ছিল। TBT বেশি মানে INP খারাপ হওয়ার ঝুঁকি বেশি, কিন্তু দুটো এক জিনিস নয়।
বাজারঘরের পণ্যের পাতা: কোথায় সময় যায়, কোথায় cache বাঁচায়
Where time goes on a product page, and which cache layer answers
এই পর্ব পুরো পাতার, production-এর দৃষ্টি। কয়েকটি টুকরো আগে আলাদা করে শিখেছেন, সেগুলো আবার শেখানো হবে না: React-এর ভেতরের re-render আর INP-এর শুরু পর্ব ২৩-এ, rendering কৌশল আর TTFB পর্ব ৫৫-এ, hydration-এর খরচ পর্ব ৫৬-এ, বাংলা font পর্ব ৫৯-এ, ছবি বানানোর pipeline পর্ব ৬৪-এ, আর bundle ভাগ করা পর্ব ৭৯-এ (Build আর bundle). এখানে সব একসাথে, একটি আসল build-এর ওপর মেপে।
২ · মাপা — Edge-এ Lighthouse CLI আর বাজারঘরের পণ্যের পাতা
প্রথম নিয়ম: dev server মাপবেন না। next dev কোড minify করে না, অতিরিক্ত পরীক্ষা চালায়, প্রতি অনুরোধে compile করে। মাপতে হয় next build আর next start-এর ওপর, যেমন production-এ চলে। এই পর্বের ল্যাবে বাজারঘরের ইলিশের পাতার তিনটি সংস্করণ একই project-এ রাখা হয়েছে, যাতে একই server-এ পাশাপাশি মাপা যায়:
| Route | কী আছে |
|---|---|
/slow/ilish (আগে) | head-এ সাধারণ stylesheet আর async ছাড়া একটি “chat widget” script (৩০০ ms main thread আটকায়), hero ছবি ৩০০০×২২৫০ JPEG (৮৩১ KB), width-height নেই, loading="lazy"; বাংলা font CSS-এর @font-face দিয়ে; ১.২ সেকেন্ড পরে আসা অফার-ব্যানার; ভারী “কার্টে যোগ” handler |
/fast/ilish (প্রথম চেষ্টা) | next/image, next/font, ব্যানারের জায়গা রাখা, handler ভাগ করা; widget শুধু next/script-এর lazyOnload-এ সরানো |
/final/ilish (পরে) | উপরের সব, আর widget শুধু “চ্যাটে প্রশ্ন করুন” বোতামে ক্লিকের পরে আসে |
লেখার সময়ের version: Node 24.15.0, Next.js 16.4.4, React 19.3.1, Lighthouse 13.6.0 (npm-এর latest), web-vitals 6.2.3, @lhci/cli 0.16.0, Microsoft Edge 156. Server চলেছে শুধু 127.0.0.1-এ। Hero ছবিটি একটি কৃত্রিম ছবি (gradient আর কিছু আকার, পর্ব ৬৪-এর ল্যাবে বানানো); আসল ছবির চেয়ে এটি অনেক বেশি সংকুচিত হয়, তাই নিচের byte-এর সঞ্চয় আসল ছবিতে কম হবে। Lighthouse চালানোর সত্যিকারের কমান্ড:
$ npm run build && npm start # next start -H 127.0.0.1 -p 48417
$ export CHROME_PATH="C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
$ npx lighthouse@13.6.0 http://127.0.0.1:48417/slow/ilish \
--only-categories=performance --output=json --output=html \
--output-path=./lh/slow --chrome-flags="--headless=new"
কোনো --preset না দিলে Lighthouse মোবাইল মাপে। JSON report-এর configSettings-এ দেখা যায় আসলে কী নকল করা হয়েছে (সত্যিকারের run থেকে):
formFactor: mobile screen: 412 × 823, deviceScaleFactor 1.75
throttlingMethod: simulate
throttling: rttMs 150, throughputKbps 1638.4, cpuSlowdownMultiplier 4
hostUserAgent: … Edg/156.0.0.0
অর্থাৎ প্রতিটি round trip ১৫০ ms, download প্রায় ১.৬ Mbps, আর CPU আপনার কম্পিউটারের চার ভাগের এক ভাগ গতির। “simulate” মানে পাতাটি দ্রুত চালিয়ে একটি মডেল দিয়ে ধীর নেটওয়ার্কের সময় হিসাব করা: দ্রুত, স্থির, কিন্তু অনুমান। প্রতিটি সংস্করণ তিনবার চালানো হয়েছে; JSON থেকে একটি ছোট script মানগুলো বের করেছে (সত্যিকারের run):
slow 1 score 67 FCP 2.1 s LCP 6.5 s TBT 40 ms CLS 0.178 bytes 1,030 KiB
slow 2 score 67 FCP 2.1 s LCP 6.5 s TBT 40 ms CLS 0.178 bytes 1,030 KiB
slow 3 score 87 FCP 2.1 s LCP 2.6 s TBT 20 ms CLS 0.178 bytes 1,030 KiB
fast 1 score 75 FCP 0.8 s LCP 2.3 s TBT 1,110 ms CLS 0 bytes 237 KiB
fast 2 score 75 FCP 0.8 s LCP 2.3 s TBT 1,110 ms CLS 0 bytes 237 KiB
fast 3 score 75 FCP 0.8 s LCP 2.3 s TBT 1,110 ms CLS 0 bytes 237 KiB
final 1 score 98 FCP 0.8 s LCP 2.3 s TBT 30 ms CLS 0 bytes 235 KiB
final 2 score 98 FCP 0.8 s LCP 2.4 s TBT 50 ms CLS 0 bytes 235 KiB
final 3 score 98 FCP 0.8 s LCP 2.3 s TBT 40 ms CLS 0 bytes 235 KiB
তিনটি শিক্ষা এখানেই। এক, একবার চালানো যথেষ্ট নয়: “আগে” পাতার LCP একবার ২.৬ সেকেন্ড, দুবার ৬.৫ সেকেন্ড। Lazy ছবি কখন খোঁজা হবে, তা প্রতিবার একটু আলাদা। তাই কয়েকবার চালিয়ে মাঝের মান (median) নিন। দুই, “প্রথম চেষ্টা” score বাড়িয়েছে মাত্র ৮: LCP আর CLS সারল, কিন্তু TBT ৪০ ms থেকে ১,১১০ ms হলো। widget-কে lazyOnload-এ পাঠানোয় তার ৩০০ ms-এর কাজ (৪ গুণ ধীর CPU-তে ১,২০০ ms) FCP-এর আগে না হয়ে পরে হলো, আর TBT ঠিক FCP-এর পরের আটকানো সময়ই গোনে। কাজ সরালে খরচ সরে, মুছে যায় না। তিন, “পরে” পাতায় widget শুধু চাইলে আসে, তাই TBT ৪০ ms, score ৯৮। সংখ্যাগুলো এই ল্যাবের একটি দিনের run; অন্য যন্ত্রে মান বদলাবে।
মূল audit — Lighthouse কী বলল
Score-এর চেয়ে কাজের হলো audit-এর তালিকা। “আগে” পাতার প্রথম run-এর ব্যর্থ audit-গুলো থেকে মূলগুলো (সত্যিকারের run, title Lighthouse-এর নিজের ভাষায়):
Render-blocking requests Est savings of 1,460 ms
/legacy.css /widget.js
Improve image delivery Est savings of 772 KiB
This image file is larger than it needs to be (3000x2250) for its
displayed dimensions (665x499). Use responsive images …
LCP request discovery
fetchpriority=high should be applied ✗
Request is discoverable in initial document ✓
LCP resources shouldn’t use loading=lazy ✗
Image elements do not have explicit `width` and `height`
Layout shift culprits
Media element lacking an explicit size · Web font loaded
Reduce JavaScript execution time 1.4 s (/widget.js প্রায় ১.২ s)
পরের তিনটি অংশ ঠিক এগুলোই সারাবে। লক্ষ করুন “665x499”: ছবিটি পর্দায় ৩৮০ CSS pixel চওড়া, আর ফোনের deviceScaleFactor ১.৭৫, তাই ৩৮০ × ১.৭৫ = ৬৬৫ আসল pixel লাগে, ৩০০০ নয়।
Score-কে লক্ষ্য বানাবেন না। Score কয়েকটি lab মাপের ওজন দেওয়া গড়, version-ভেদে বদলায়। ১০০ পেয়ে আসল ক্রেতার INP খারাপ থাকা খুবই সম্ভব, কারণ lab-এ কেউ ক্লিক করেনি। Score দিয়ে দিক বুঝুন, audit দিয়ে কাজ ঠিক করুন, আর রায় দিন field data দিয়ে।
৩ · মাঠের ডেটা — web-vitals library আর নিজের endpoint
Field data পাওয়ার দুটি পথ। একটি হলো Google-এর CrUX (Chrome User Experience Report), যেখানে যথেষ্ট ভিজিটর থাকলে Chrome ব্যবহারকারীদের সমষ্টিগত ডেটা পাওয়া যায়; নতুন বা ছোট সাইটের জন্য প্রায়ই ডেটা থাকে না। অন্যটি নিজে মাপা: Chrome দলের ছোট web-vitals library প্রতিটি ক্রেতার browser-এ মাপে, আর আপনি মানগুলো নিজের server-এ পাঠান। বাজারঘরের একটি client component:
'use client';
import { useEffect } from 'react';
import { onCLS, onINP, onLCP, onFCP, onTTFB, type Metric } from 'web-vitals';
function send(metric: Metric) {
const body = JSON.stringify({
page: location.pathname,
name: metric.name, // 'LCP' | 'INP' | 'CLS' | 'FCP' | 'TTFB'
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
id: metric.id,
});
// পাতা লুকানো বা বন্ধ হওয়ার মুহূর্তেও যায়; না পারলে keepalive fetch
if (!navigator.sendBeacon('/api/vitals', body)) {
fetch('/api/vitals', { method: 'POST', body, keepalive: true });
}
}
export function WebVitals() {
useEffect(() => {
onCLS(send); onINP(send); onLCP(send); onFCP(send); onTTFB(send);
}, []);
return null;
}ল্যাবের ফাইলে CLS-কে তিন দশমিকে গোল করা হয়েছে, বাকিটা একই। navigator.sendBeacon কেন? CLS আর INP-এর চূড়ান্ত মান জানা যায় কেবল তখন, যখন ক্রেতা পাতা ছেড়ে যান বা অন্য app-এ যান; library তখন visibilitychange-এ মান দেয়। সেই মুহূর্তে সাধারণ fetch কেটে যেতে পারে, beacon browser নিজের হাতে রেখে পাঠায়। Next.js-এর নিজস্ব useReportWebVitals hook-ও আছে। গ্রহণের দিকে একটি Route Handler, যা যাচাই করে একটি ফাইলে লাইন যোগ করে:
import { appendFile } from 'node:fs/promises';
const NAMES = new Set(['CLS', 'INP', 'LCP', 'FCP', 'TTFB']);
export async function POST(req: Request) {
const text = await req.text();
if (text.length > 2000) return new Response(null, { status: 413 });
let m: { name?: string; value?: number; page?: string; rating?: string };
try { m = JSON.parse(text); } catch { return new Response(null, { status: 400 }); }
if (!m.name || !NAMES.has(m.name) || typeof m.value !== 'number') {
return new Response(null, { status: 400 });
}
const line = JSON.stringify({ page: m.page, name: m.name, value: m.value, rating: m.rating });
await appendFile('vitals.log', line + '\n');
return new Response(null, { status: 204 });
}Endpoint সবার জন্য খোলা, তাই আকার সীমা আর নামের তালিকা; production-এ rate limit (পর্ব ৪৯) আর ফাইলের বদলে database বা পর্ব ৩১-এর structured log লাগবে, আর ব্যক্তিগত তথ্য (নাম, ফোন) কখনো এখানে নয়। Dashboard আর alert পর্ব ৮৩-এর (Monitoring) বিষয়।
এরপর Playwright দিয়ে Edge চালিয়ে একজন “ক্রেতা” বানানো হলো: ৪১২ px চওড়া পর্দা, CDP দিয়ে CPU ৪ গুণ ধীর, font ফাইল ১ সেকেন্ড দেরিতে (ধীর নেটওয়ার্কের নকল), পাতা খুলে আড়াই সেকেন্ড পরে “কার্টে যোগ করুন” ক্লিক, তারপর পাতা লুকানো। Headless browser-এ tab কখনো নিজে থেকে লুকায় না, তাই script নিজেই visibilitychange ঘটনা ঘটিয়েছে; আসল ফোনে ক্রেতা অন্য app-এ গেলে browser এটি নিজেই করে। vitals.log-এ যা জমল (সত্যিকারের run):
{"page":"/slow/ilish","name":"FCP","value":528,"rating":"good"}
{"page":"/slow/ilish","name":"TTFB","value":8,"rating":"good"}
{"page":"/slow/ilish","name":"LCP","value":744,"rating":"good"}
{"page":"/slow/ilish","name":"INP","value":208,"rating":"needs-improvement"}
{"page":"/slow/ilish","name":"CLS","value":0.229,"rating":"needs-improvement"}
{"page":"/final/ilish","name":"FCP","value":328,"rating":"good"}
{"page":"/final/ilish","name":"TTFB","value":6,"rating":"good"}
{"page":"/final/ilish","name":"LCP","value":328,"rating":"good"}
{"page":"/final/ilish","name":"INP","value":16,"rating":"good"}
{"page":"/final/ilish","name":"CLS","value":0,"rating":"good"}
“আগে” পাতার LCP এখানে মাত্র ৭৪৪ ms, অথচ Lighthouse বলেছিল ৬.৫ সেকেন্ড। কোনটি সত্য? দুটোই, ভিন্ন প্রশ্নের উত্তর। এই “ক্রেতা” একই কম্পিউটারে, localhost-এ, নেটওয়ার্কের দেরি প্রায় শূন্য; Lighthouse ১৫০ ms RTT আর ১.৬ Mbps ধরে হিসাব করেছে। আসল field data আসত হাজারো ভিন্ন ফোন আর নেটওয়ার্ক থেকে, আর আপনি দেখতেন তাদের 75th percentile. অন্যদিকে CLS আর INP-এ নেটওয়ার্কের ভূমিকা কম, তাই এখানেও “আগে” পাতা ধরা পড়েছে: CLS ০.২২৯, INP ২০৮ ms. শেষ লাইনগুলো দেখায় সারানোর পরে তিনটিই “good”.
৪ · LCP — অনুরোধ থেকে বড় ছবি আঁকা পর্যন্ত
LCP-কে চারটি টুকরোয় ভাঙা যায়, আর Lighthouse 13-এর “LCP breakdown” ঠিক এই চারটিই দেখায়: TTFB (HTML-এর প্রথম byte), resource load delay (TTFB থেকে LCP ছবির অনুরোধ শুরু হওয়া পর্যন্ত), resource load duration (ছবি নামতে কত সময়) আর element render delay (ছবি এসে গেছে, তবু আঁকা হয়নি, কারণ CSS বা script আটকে রেখেছে)। প্রতিটির ওষুধ আলাদা, তাই আগে দেখুন কোন টুকরো বড়। নিচের stepper-এ “আগে” পাতার পথ একটি শিক্ষণ মডেলে হিসাব করা হয়েছে: Lighthouse-এর মতো RTT ১৫০ ms, ১,৬৩৮.৪ Kbps, CPU ৪ গুণ ধীর, আর সংযোগে চারটি round trip (DNS, TCP, TLS, অনুরোধ)। সংখ্যাগুলো একটি scratch script-এ গোনা; Lighthouse-এর আসল simulation আরও খুঁটিনাটি ধরে, তবু ফল কাছাকাছি (মডেলে FCP ১,৯৬৪ ms আর LCP ৬,১৭৩ ms, Lighthouse-এ ২.১ s আর ৬.৫ s).
- অনুরোধ
0 msDNS, TCP, TLS - HTML
610 msTTFB - CSS + widget
764 msrender-blocking - FCP
1,964 msলেখা দেখা গেল - LCP
6,173 msইলিশের ছবি
DNS 150 ms
TCP 150 ms
TLS 150 ms
GET 150 ms + server 10 ms
TTFB = 4 × 150 + 10 = 610 ms
<head> <link rel="stylesheet" href="/legacy.css"> ← আঁকা থামায় <script src="/widget.js"></script> ← parser থামায় </head> <img src="/ilish-photo.jpg" loading="lazy">
legacy.css 1 RTT + 862 B → 764 ms
widget.js 1 RTT + 619 B → প্রায় একই সময়
widget চলে 300 ms × CPU 4 = 1,200 ms
764 + 1,200 = 1,964 ms
লেখা আঁকা হলো।
lazy ছবি: layout হওয়ার পরে browser দেখে
"ছবিটা পর্দার ভেতরে" → এখন অনুরোধ।
resource load delay ≈ 1,354 ms
831,320 B × 8 ÷ 1,638,400 b/s = 4,059 ms
1,964 + 150 (RTT) + 4,059 = 6,173 ms
পরে: AVIF 750w = 10,413 B → 51 ms
- অনুরোধ ছাড়ল, ০ ms. নতুন ক্রেতার ফোনে আগে DNS, তারপর TCP আর TLS-এর হাত মেলানো, তারপর আসল GET. প্রতিটি একটি করে round trip (মডেলে ১৫০ ms), আর server-এর নিজের কাজ ১০ ms. TTFB দাঁড়াল ৬১০ ms; এর অর্ধেকের বেশি সংযোগ গড়তেই। CDN কাছে থাকলে বা সংযোগ আগে থেকে খোলা থাকলে এই অংশ কমে।
- HTML এল, ৬১০ ms. Browser পড়তে শুরু করল আর head-এ দুটি বাধা পেল: সাধারণ stylesheet, যা না আসা পর্যন্ত কিছু আঁকা যাবে না, আর async ছাড়া script, যা না চলা পর্যন্ত HTML পড়াই থেমে থাকবে। ছবির tag-এ
loading="lazy": browser ছবিটির অনুরোধ এখনই করবে না। - Render-blocking, ৬১০ থেকে ১,৯৬৪ ms. দুটি ছোট ফাইল এক round trip-এ এল (৭৬৪ ms). কিন্তু widget-এর ৩০০ ms কাজ ৪ গুণ ধীর CPU-তে ১,২০০ ms. এই পুরো সময় পর্দা সাদা। Lighthouse এটিকেই “Render-blocking requests” আর “Reduce JavaScript execution time” বলেছিল।
- FCP, ১,৯৬৪ ms. লেখা দেখা গেল। Layout হওয়ার পরে browser বুঝল lazy ছবিটি পর্দার ভেতরেই আছে, তাই এখন অনুরোধ পাঠাল। TTFB থেকে ছবির অনুরোধ পর্যন্ত এই অপেক্ষাটাই resource load delay, প্রায় ১,৩৫৪ ms; প্রথম পর্দার hero ছবিতে
loading="lazy"দেওয়ার শাস্তি। - LCP, ৬,১৭৩ ms. ৮৩১,৩২০ byte × ৮ ÷ ১,৬৩৮,৪০০ bit/s = ৪,০৫৯ ms শুধু নামতে, এক round trip যোগ করে ৬,১৭৩ ms. একই ছবি ৭৫০ px চওড়া AVIF-এ ১০,৪১৩ byte, নামতে ৫১ ms. ছবির আকারই সবচেয়ে বড় টুকরো ছিল, তারপর দেরিতে খোঁজা, তারপর render-blocking script.
চারটি সংশোধন, একটি একটি করে
১. ছবি আগে খোঁজা আর অগ্রাধিকার দেওয়া। LCP ছবিতে কখনো loading="lazy" নয়। HTML পড়ার সময় browser জানে না কোন ছবি বড় হবে, তাই সাধারণত ছবিকে কম অগ্রাধিকার দেয়। fetchpriority="high" বলে দেয় “এটিই আসল”। ছবি যদি HTML-এ সরাসরি না থাকে (CSS background, বা JavaScript পরে বসায়), তাহলে head-এ preload লাগে, নইলে browser অনেক দেরিতে জানবে। সাদা HTML-এ:
<link rel="preload" as="image" fetchpriority="high"
imagesrcset="/img/ilish-640.avif 640w, /img/ilish-960.avif 960w"
imagesizes="(max-width: 672px) 100vw, 640px">
…
<img src="/img/ilish-640.jpg" width="640" height="480" fetchpriority="high"
srcset="/img/ilish-640.avif 640w, /img/ilish-960.avif 960w"
sizes="(max-width: 672px) 100vw, 640px" alt="বরফে রাখা দুটি ইলিশ">২. ঠিক মাপের ছবি, আধুনিক format-এ। srcset browser-কে কয়েকটি মাপের তালিকা দেয়, আর sizes বলে ছবিটি পর্দায় কত চওড়া হবে; দুটো মিলিয়ে browser নিজের পর্দা আর DPR অনুযায়ী বেছে নেয় (তত্ত্ব আছে HTML ও CSS-এর responsive images পর্বে)। sizes ভুল দিলে browser বড়টাই নামাবে। AVIF আর WebP একই চেহারায় JPEG-এর চেয়ে সাধারণত অনেক ছোট; কোনটি কত ছোট, ছবির ধরনের ওপর নির্ভর করে, তাই নিজের ছবিতে মাপুন।
৩. Render-blocking কমানো। জরুরি CSS ছোট রাখুন, আর তৃতীয় পক্ষের script head-এ async ছাড়া কখনো নয়। ৪. TTFB. পণ্যের পাতা static বা ISR হলে TTFB ছোট থাকে (পর্ব ৫৫); প্রতি অনুরোধে database-এ ধীর query চললে বাকি সব সংশোধন সেই দেরির পেছনে দাঁড়িয়ে থাকে।
next/image: বাজারঘরের “পরে” পাতা
Next.js-এ এই কাজগুলো next/image অনেকটাই করে দেয়। Static import করলে Next.js ছবির width আর height নিজেই জানে, ফাইলের নামে hash বসায়, আর /_next/image দিয়ে চাওয়া মাপে বদলে দেয়। AVIF চালু করতে config-এ format দিতে হয়, কারণ ডিফল্ট শুধু ['image/webp']:
import Image from 'next/image';
import ilish from '../../../ilish-photo.jpg'; // static import: মাপ আর hash জানা
<Image
className="hero"
src={ilish}
alt="বরফে রাখা দুটি ইলিশ"
sizes="(max-width: 672px) 100vw, 640px"
fetchPriority="high" // LCP ছবি: আগে আর বেশি অগ্রাধিকারে
loading="eager" // next/image-এর ডিফল্ট lazy, এখানে নয়
/>
// next.config.ts
images: { formats: ['image/avif', 'image/webp'] },Next.js 16-এর docs অনুযায়ী পুরোনো priority prop বাতিলের পথে (deprecated), তার জায়গায় preload এসেছে, আর docs বলে বেশিরভাগ ক্ষেত্রে loading="eager" বা fetchPriority="high"-ই যথেষ্ট। AI বা পুরোনো blog প্রায়ই এখনো priority লেখে; আপনার version-এর docs দেখে নিন। Build-এর আসল HTML-এ (সত্যিকারের run, URL ছোট করা) head-এ একটি preload নিজে থেকেই এসেছে, আর img পেয়েছে width, height আর পুরো srcset:
<link rel="preload" as="image" imageSrcSet="/_next/image?url=…ilish-photo.0_vfw54rs5_s3.jpg&w=640&q=75 640w, …&w=750 750w, …&w=828 828w, … 3840w"
imageSizes="(max-width: 672px) 100vw, 640px" fetchPriority="high"/>
<img alt="বরফে রাখা দুটি ইলিশ" fetchPriority="high" loading="eager" width="3000" height="2250"
decoding="async" class="hero" sizes="(max-width: 672px) 100vw, 640px" srcSet="…"/>
Lighthouse-এর মোবাইলে ছবিটি ৩৮০ px চওড়া, DPR ১.৭৫, তাই browser ৭৫০w বেছে নিল। একই URL-এ তিন রকম Accept header দিয়ে curl-এর আসল উত্তর (একই ৭৫০ px ছবি, ভিন্ন format):
Accept: image/avif,image/webp,*/* → Content-Type: image/avif Content-Length: 9989
Accept: image/webp,*/* → Content-Type: image/webp Content-Length: 10194
Accept: */* → Content-Type: image/jpeg Content-Length: 15987
মূল ফাইল /ilish-photo.jpg (3000×2250) → Content-Length: 831036
Lighthouse-এর মাপে মোট পাতার ওজন ১,০৩০ KiB থেকে ২৩৫ KiB, আর মাঝের LCP ৬.৫ s থেকে ২.৩ s. “পরে” পাতার LCP কেন ০.৮ s নয়, যখন মডেলে ছবি ৮১১ ms-এ চলে আসার কথা? সম্ভবত simulation একই সরু লাইনে JavaScript (প্রায় ১৪০ KB gzip) আর font (৭৭ KB)-এর ভাগও ধরে। ছবির পরে পরের বোঝা তাই JavaScript, পর্ব ৭৯-এর (Build আর bundle) বিষয়।
/_next/image server-এর CPU-তে ছবি বানায়; অনেক ছবি আর অনেক server হলে পর্ব ৬৪-এর মতো আগেই মাপগুলো বানিয়ে CDN থেকে দেওয়া আর loader ব্যবহার প্রায়ই বেশি টেকসই। Proxy বা CDN থাকলে Accept আর Vary: Accept ঠিকমতো যাচ্ছে কি না দেখুন।
৫ · CLS — মাপহীন ছবি, font আর দেরিতে আসা ব্যানার
বাসে উঠে সিটে বসেছেন, এমন সময় কন্ডাক্টর বললেন “একটু চাপেন”, আর আপনার জায়গা সরে গেল। পাতায় এটাই layout shift: আপনি “কার্টে যোগ” বোতামে আঙুল নিচ্ছেন, ঠিক তখন ওপরে একটি ছবি এসে বোতামটি নিচে ঠেলে দিল, আর আঙুল পড়ল অন্য কিছুতে। নিচের পাতাটি নিজেই চালিয়ে দেখুন। এতে একটি ছবি ৩০০ ms পরে আসে, যার মাপ আগে জানা ছিল না, আর PerformanceObserver browser-এর আসল layout-shift ঘটনা ধরে:
<p style="background:#0d1f3c;color:#fff;padding:8px">বাজারঘর · পদ্মার ইলিশ</p>
<div id="slot"></div>
<p id="price" style="background:#fde68a;padding:8px">৳ ১,২৫০ — কার্টে যোগ করুন</p>
<script>
// ছবির জায়গা: মাপ জানা নেই (চাইলে slot-এ style="aspect-ratio:4/3" দিয়ে আবার চালান)
let total = 0;
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
total += e.value;
console.log(`layout-shift ${e.value.toFixed(4)}, সরেছে ${e.sources.length}টি element`);
}
}).observe({ type: "layout-shift", buffered: true });
const price = document.getElementById("price");
setTimeout(() => console.log("শুরুতে দামের সারির offsetTop =", price.offsetTop, "px"), 100);
setTimeout(() => {
const c = document.createElement("canvas"); // নেটওয়ার্ক নেই, তাই ছবি বানাই canvas থেকে
c.width = 320; c.height = 240;
const g = c.getContext("2d");
g.fillStyle = "#3b6e8f"; g.fillRect(0, 0, 320, 240);
const img = new Image();
img.alt = "ইলিশ"; img.style.width = "100%"; img.style.display = "block";
img.onload = () => setTimeout(() => console.log("ছবি এল; offsetTop =", price.offsetTop, "px; মোট shift =", total.toFixed(4)), 400);
img.src = c.toDataURL();
document.getElementById("slot").appendChild(img);
}, 300);
</script>এই পাতার preview-তে আসল ফল: শুরুতে দামের সারির offsetTop = 69 px, তারপর layout-shift 0.2176, আর শেষে offsetTop = 739 px; মোট shift = 0.2176. ছবির জায়গা আগে শূন্য ছিল, ছবি এসে দামের সারিকে ৬৭০ px নামিয়ে দিল, আর browser নিজেই shift-টি মেপে দিল; একা এটিই ০.১-এর সীমা পেরিয়েছে। মান preview-এর আকারভেদে একটু বদলায়। এবার মন্তব্যের কথামতো slot-এ style="aspect-ratio:4/3" দিয়ে আবার চালান: শুরু থেকেই জায়গা থাকায় কোনো layout-shift লাইন আসবে না, মোট ০।
Score-এর হিসাব: impact আর distance
একটি shift-এর score = impact fraction × distance fraction. Impact fraction হলো সরে যাওয়া element-গুলো আগে আর পরে মিলিয়ে viewport-এর কত ভাগ জুড়েছিল। Distance fraction হলো সবচেয়ে বেশি কতটা সরল, viewport-এর বড় দিকের (এখানে উচ্চতার) ভাগ হিসেবে। বাজারঘরের “আগে” পাতার আসল run থেকে, ৪১২ × ৮২৩ viewport-এ, মাপহীন ছবি (৩৮০ × ২৮৫) আসায় চারটি element ২৮৫ px নামল। নিচের stepper সেই সংখ্যাগুলো দিয়ে হিসাব করে, তারপর সংশোধন দেখায়:
y 0 ┌ বাজারঘর ─────────────┐
y 50 │ পদ্মার ইলিশ (h1) │
y 152 │ [কার্টে যোগ করুন] │ ← আঙুল এখানে যাচ্ছে
y 200 │ ৳ ১,২৫০ / কেজি │
y 265 │ বিবরণ … │
y 823 └──────────────────────┘ (viewport-এর নিচ)
y 152 │ 🐟 ছবি (380 × 285) │
y 437 │ [কার্টে যোগ করুন] │ 152 → 437
y 485 │ ৳ ১,২৫০ / কেজি │ 200 → 485
y 550 │ বিবরণ … │ 265 → 550
layout-shift entry: value 0.1747
আগে-পরের জায়গা মিলিয়ে উচ্চতা = 451 px, চওড়া 380 px
impact = 451 × 380 ÷ (412 × 823) = 0.5054
distance = 285 ÷ 823 = 0.3463
score = 0.5054 × 0.3463 = 0.1750 (মাপা 0.1747)
<Image src={ilish} … /> → width="3000" height="2250"
CSS: width:100%; height:auto
browser আগেই অনুপাত জানে: 380 × 3/4 = 285 px রাখে
y 437 [কার্টে যোগ করুন] ← প্রথম আঁকা থেকেই এখানে
shift = 0
আগে: ব্যানার 1.2 s পরে, main 50 → 96 (46 px)
impact 0.9186 × distance 0.0559 = 0.0513
পরে: <div class="promo" style="min-height:48px">
খালি থাকলেও জায়গা রাখা → shift 0
field-এ CLS: 0.229 → 0
- প্রথম আঁকা। ছবির tag আছে কিন্তু width-height নেই, তাই ছবি আসার আগে browser তার উচ্চতা ০ ধরে। “কার্টে যোগ করুন” বোতাম y ১৫২-তে। ক্রেতা ঠিক এটাই দেখছেন আর আঙুল বাড়াচ্ছেন।
- ছবি এল, ৬৯৬ ms-এ। ৩৮০ px চওড়া ছবি ৪:৩ অনুপাতে ২৮৫ px উঁচু। নিচের সবকিছু ঠিক ২৮৫ px নামল: বোতাম ১৫২ থেকে ৪৩৭. Browser একটি layout-shift entry দিল, মান ০.১৭৪৭; একা এটিই “ভালো”-র সীমা ০.১ পেরিয়ে গেছে।
- হিসাব মেলানো। সরে যাওয়া চারটি element আগে আর পরে মিলিয়ে ৪৫১ px উচ্চতা জুড়েছিল, ৩৮০ px চওড়া; viewport-এর ৫০.৫৪%. সবচেয়ে বড় সরা ২৮৫ px, viewport-উচ্চতার ৩৪.৬৩%. গুণফল ০.১৭৫০, মাপা মান ০.১৭৪৭ (pixel গোল করার পার্থক্য)।
- সংশোধন: মাপ জানানো।
next/imagestatic import থেকে width ৩০০০ আর height ২২৫০ বসায়; CSS-এwidth:100%; height:autoথাকায় browser ছবি আসার আগেই ৩৮০ × ৩/৪ = ২৮৫ px জায়গা রাখে। বোতাম শুরু থেকেই y ৪৩৭-এ, কিছু সরে না। সাধারণ HTML-এwidthআরheightattribute, বা CSS-এরaspect-ratio, একই কাজ করে। - সংশোধন: ব্যানারের জন্য জায়গা। দেরিতে আসা অফার-ব্যানার ৪৬ px নামিয়েছিল প্রায় পুরো main (impact ০.৯১৮৬ × distance ০.০৫৫৯ = ০.০৫১৩)। খালি অবস্থাতেই
min-height:48pxরাখলে পরে লেখা এলেও কিছু সরে না। দুটি সংশোধনের পরে field-এর CLS ০.২২৯ থেকে ০।
বিজ্ঞাপনের ক্ষেত্রেও একই নিয়ম: বিজ্ঞাপন আসবে কি না, কত বড় হবে, আগে জানা যায় না, তাই সাধারণত সবচেয়ে সম্ভাব্য মাপের জায়গা আগে রাখা হয়। Cookie নোটিশ বা chat বোতাম position: fixed-এ ভাসিয়ে রাখলে কিছু ঠেলে না। আর ক্লিকের ফল হিসেবে কিছু খুললে, ক্লিকের ৫০০ ms-এর ভেতরের shift-এ hadRecentInput চিহ্ন থাকে আর CLS-এ গোনা হয় না; ক্রেতা নিজে চেয়েছেন, তাই সেটা ধাক্কা নয়।
Session window: কেন একই shift দুবার দুই রকম CLS দিল
CLS সব shift যোগ করে না। পরপর shift-গুলো একটি session window-এ জোড়া হয়, যতক্ষণ দুটির মাঝে ১ সেকেন্ডের কম ফাঁক আর পুরো window ৫ সেকেন্ডের কম; সবচেয়ে বড় window-এর যোগফলই CLS. বাজারঘরের “আগে” পাতার দুটি আসল run-এ একই তিনটি shift এল, শুধু সময় একটু আলাদা, আর web-vitals দুবার দুই রকম CLS পাঠাল (০.২২৯ আর ০.১৭৫)। নিচের মডেল সেটি ব্যাখ্যা করে:
// শিক্ষণ মডেল: web.dev-এর সংজ্ঞায় CLS = সবচেয়ে বড় session window-এর যোগফল
// window ভাঙে যদি আগের shift থেকে ১ সেকেন্ড বা বেশি ফাঁক, অথবা window ৫ সেকেন্ড ছাড়ায়
function cls(entries) {
let best = 0, cur = 0, first = 0, prev = 0, windows = [];
for (const e of entries) {
if (e.input) continue; // ক্লিকের ৫০০ ms-এর ভেতরের shift বাদ
if (cur > 0 && (e.t - prev >= 1000 || e.t - first >= 5000)) {
windows.push(cur); cur = 0;
}
if (cur === 0) first = e.t;
cur += e.v; prev = e.t;
best = Math.max(best, cur);
}
if (cur > 0) windows.push(cur);
return { cls: +best.toFixed(4), windows: windows.map((w) => +w.toFixed(4)) };
}
// বাজারঘরের "আগে" পাতার একই তিনটি shift, দুটি ভিন্ন run-এর সময়ে (ms)
const runA = [
{ t: 696, v: 0.1747 }, // মাপহীন ছবি এসে লেখা ঠেলে নামাল
{ t: 1448, v: 0.0029 }, // বাংলা font এসে লেখা সামান্য সরাল
{ t: 1976, v: 0.0513 }, // দেরিতে আসা ব্যানার পুরো main নামাল
{ t: 4192, v: 0.0168, input: true }, // ক্লিকের ফল: গোনা হয় না
];
const runB = [{ t: 517, v: 0.1747 }, { t: 1560, v: 0.0029 }, { t: 1992, v: 0.0513 }];
console.log("run A", JSON.stringify(cls(runA)));
console.log("run B", JSON.stringify(cls(runB)));
const fixed = [{ t: 3848, v: 0.0163, input: true }];
console.log("পরে ", JSON.stringify(cls(fixed)));
const rating = (x) => (x <= 0.1 ? "ভালো" : x <= 0.25 ? "উন্নতি দরকার" : "খারাপ");
console.log("rating:", rating(cls(runA).cls), "/", rating(cls(runB).cls), "/", rating(0));আউটপুট run A {"cls":0.2289,"windows":[0.2289]}: তিনটি shift-এর প্রতিটি আগেরটির ১ সেকেন্ডের কম পরে (৬৯৬ → ১,৪৪৮ → ১,৯৭৬), তাই একটিই window, যোগফল ০.২২৮৯, যা web-vitals-এর পাঠানো ০.২২৯-এর সাথে মেলে। run B {"cls":0.1747,"windows":[0.1747,0.0542]}: ৫১৭ থেকে ১,৫৬০-এর ফাঁক ১,০৪৩ ms, তাই window ভাঙল; সবচেয়ে বড়টি ০.১৭৪৭, web-vitals-এর ০.১৭৫. ক্লিকের পরের ০.০১৬৮ দুটি ক্ষেত্রেই বাদ। পরে {"cls":0,…}: শুধু ক্লিকের ফলের shift ছিল। শেষ লাইন: দুই “আগে” run-ই “উন্নতি দরকার”, “পরে” “ভালো”। শিক্ষা: একই ভুল, ভিন্ন সময়ে, ভিন্ন CLS; field-এ তাই অনেক page view-এর p75 লাগে।
বাংলা font আর size-adjust: একটি আসল মাপ
Web font আসার আগে browser একটি fallback font-এ লেখা দেখায় (font-display: swap), তারপর আসল font এলে বদলায়। দুই font-এর মাপ আলাদা হলে লাইন ভাঙা আর উচ্চতা বদলায়, লেখা সরে। next/font এর জন্য নিজে থেকে একটি fallback @font-face বানায়, যাতে size-adjust আর ascent, descent override থাকে। বাজারঘরের build-এ (Noto Sans Bengali Bold, next/font/local) আসল তৈরি CSS:
@font-face{font-family:bn;src:url(../media/NotoSansBengali_Bold-s.p.2imrbn8bck5ne.ttf)format("truetype");font-display:swap}
@font-face{font-family:bn Fallback;src:local(Arial);ascent-override:91.7%;descent-override:40.8%;line-gap-override:0.0%;size-adjust:100.0%}
._8fPmIW_className{font-family:bn,bn Fallback}
লক্ষ করুন local(Arial). next/font/local-এর adjustFontFallback-এর ডিফল্ট 'Arial', আর Arial-এ বাংলা অক্ষর নেই। তাই বাংলা লেখা আসলে দেখানো হয় system-এর কোনো বাংলা font-এ (যেমন Windows-এ সাধারণত Nirmala UI), যার মাপ এই override-এর হিসাবে নেই। ল্যাবে font ফাইল ১ সেকেন্ড দেরি করিয়ে মাপা হলো (সত্যিকারের run, Edge, CPU ৪x):
/fast/ilish, next/font, line-height ঠিক করা নেই:
shift 1073ms 0.0118 MAIN y102->98 | DIV.promo y54->50 | #text y16->12
/final/ilish, next/font + body{line-height:1.6} h1{line-height:1.4}:
(font বদলের কোনো shift নেই) CLS 0
font আসায় নাম-বারের লাইন ৪ px ছোট হলো, নিচের সব উঠে গেল। Size-adjust Latin অক্ষরের জন্য ভালো কাজ করে, বাংলার জন্য একা যথেষ্ট নয়। যা সাহায্য করেছে তা সাধারণ CSS: line-height একটি নির্দিষ্ট সংখ্যা দিলে লাইনের উচ্চতা font-এর নিজস্ব মাপের ওপর নির্ভর করে না। লম্বা অনুচ্ছেদে অক্ষরের চওড়া আলাদা হলে লাইন সংখ্যা বদলে আবার shift হতে পারে। তখন display: 'optional', subset করা ছোট woff2, বা বাংলা system font ধরে নিজের fallback ভাবুন। বাংলা font-এর বাকি কথা পর্ব ৫৯-এ, typography-র তত্ত্ব HTML ও CSS-এর font পর্বে।
৬ · INP — long task, কাজ ভাগ করা আর third-party script
রেস্তোরাঁর একজন ওয়েটার নিজে রান্নাঘরে গিয়ে দশ পদ রেঁধে তবে ফিরলেন; ততক্ষণ অন্য টেবিলে কেউ পানিও পায়নি। Browser-এর main thread ঠিক এমন একজন ওয়েটার: click handler, React render, layout, আঁকা, সবই এক লাইনে। ৫০ ms-এর বেশি একটানা কাজকে বলে long task. ক্লিকের সময় long task চললে browser পরের ছবি আঁকতে পারে না, আর INP বাড়ে। Event Timing API প্রতিটি interaction-কে তিন ভাগে মাপে: input delay (handler শুরুর আগে অপেক্ষা), processing (handler চলার সময়) আর presentation delay (handler শেষ থেকে পরের আঁকা পর্যন্ত)। React-এর ভেতরের দিক, যেমন useTransition আর useDeferredValue, পর্ব ২৩-এ দেখেছেন; এখানে দেখব পুরো পাতার স্তরে।
একটি আসল ধীর handler, মাপা আর সারানো
বাজারঘরের “কার্টে যোগ করুন” বোতাম ক্লিকে একটি “আপনার জন্য আরও পণ্য” স্কোর হিসাব করে, ইচ্ছা করে ভারী। “আগে” সংস্করণে সব কাজ একটানা, তারপর UI বদল। “পরে” সংস্করণে আগে কার্টের সংখ্যা বদলায়, তারপর main thread ছেড়ে দিয়ে কাজটি ছয় টুকরোয় করে:
function yieldToMain(): Promise<void> {
const sch = (globalThis as { scheduler?: { yield?: () => Promise<void> } }).scheduler;
if (sch?.yield) return sch.yield(); // থাকলে: অগ্রাধিকার রেখে ছেড়ে দেওয়া
return new Promise((r) => setTimeout(r, 0)); // না থাকলে: পুরোনো পথ
}
async function onAdd() {
if (mode === 'slow') {
const s = scoreRecommendations(6_000_000); // সব কাজ একটানা, তারপর UI
setCount((c) => c + 1);
setScore(s);
return;
}
setCount((c) => c + 1); // আগে ক্রেতার চোখের সামনের পরিবর্তন
await yieldToMain(); // browser-কে আঁকার সুযোগ
let s = 0;
for (let part = 0; part < 6; part++) {
s += scoreRecommendations(1_000_000);
await yieldToMain(); // প্রতি টুকরোর পরে আবার ছেড়ে দেওয়া
}
setScore(s);
}Playwright দিয়ে Edge-এ, CPU ৪ গুণ ধীর করে, ক্লিকের Event Timing entry পড়া হলো (সত্যিকারের run; কলাম: ঘটনা, মোট duration, interactionId, input delay, processing, সব ms-এ):
slow/ilish ["pointerdown",232,9265,9,2] ["pointerup",232,9265,13,1] ["click",232,9265,14,200]
final/ilish ["pointerdown",32,7155,6,1] ["pointerup",32,7155,8,1] ["click",32,7155,9,4]
“আগে” পাতায় click handler নিজেই ২০০ ms চলেছে, পুরো interaction ২৩২ ms. “পরে” পাতায় handler ৪ ms, interaction ৩২ ms; বাকি ভারী কাজ পরের task-গুলোতে চলেছে, ক্রেতা ততক্ষণে সংখ্যা বদল দেখে ফেলেছেন। web-vitals-এর পাঠানো INP একই দিকে: ২০৮ ms (“উন্নতি দরকার”) থেকে ১৬ ms. আগের দুটি run-এ এসেছিল ২৩২ আর ২৮৮ ms; browser duration ৮ ms-এর ধাপে গোল করে।
scheduler.yield() main thread ছেড়ে দেয়, কিন্তু বাকি কাজটিকে task queue-র একদম পেছনে পাঠায় না; একই কাজ অগ্রাধিকার নিয়ে আবার শুরু হয়। setTimeout(0) পেছনে পাঠায়, তাই মাঝে অন্য script ঢুকে পড়তে পারে। লেখার সময়ে MDN এটিকে “Limited availability” বলে (Baseline নয়); এই ল্যাবের Edge 156-এ typeof scheduler.yield ছিল "function", কিন্তু সব browser-এ নেই। তাই উপরের মতো feature detection আর fallback রাখুন। CPU-ভারী হিসাব একেবারে main thread থেকে সরাতে Web Worker আরও ভালো পথ; event loop-এর তত্ত্ব JavaScript-এর event loop পর্বে।
INP-এর হিসাব: প্রায় সবচেয়ে খারাপটি, তারপর p75
একটি page view-এ অনেক ক্লিক থাকলে INP সবচেয়ে ধীরটি নয়, প্রায় সবচেয়ে ধীরটি: web.dev-এর নিয়মে প্রতি ৫০টি interaction-এ সবচেয়ে বড় একটি বাদ যায়, যাতে একটি অদ্ভুত ঘটনা পুরো visit-কে খারাপ না বলায়। তারপর সব page view-এর INP থেকে 75th percentile. নিচের মডেলে দুটি নিয়ম:
// শিক্ষণ মডেল: একটি page view-এর INP, তারপর অনেক page view-এর 75th percentile
function inpOfView(durations) {
const sorted = [...durations].sort((a, b) => b - a); // বড় থেকে ছোট
const skip = Math.floor(durations.length / 50); // প্রতি ৫০টিতে সবচেয়ে বড় একটি বাদ
return sorted[Math.min(skip, sorted.length - 1)];
}
function p75(values) {
const s = [...values].sort((a, b) => a - b);
return s[Math.ceil(0.75 * s.length) - 1]; // nearest-rank পদ্ধতি
}
let seed = 84; // নির্দিষ্ট seed: প্রতিবার একই ফল
const rand = () => ((seed = (seed * 16807) % 2147483647) / 2147483647);
console.log("৩টি ক্লিক [48, 208, 64] → INP", inpOfView([48, 208, 64]));
const many = Array.from({ length: 120 }, () => 40 + Math.round(rand() * 110));
many[17] = 900; many[60] = 640; many[99] = 310; // তিনটি ব্যতিক্রমী ধীর ক্লিক
const top4 = [...many].sort((a, b) => b - a).slice(0, 4);
console.log("১২০টি interaction, সবচেয়ে ধীর ৪টি:", top4.join(", "));
console.log(`বাদ যায় ${Math.floor(120 / 50)}টি → INP ${inpOfView(many)}`);
const views = [208, 16, 24, 232, 40, 56, 288, 32, 48, 120, 64, 16]; // ১২টি page view-এর INP (ms)
const v = p75(views);
console.log("১২টি page view-এর p75 INP =", v, "ms →", v <= 200 ? "ভালো" : v <= 500 ? "উন্নতি দরকার" : "খারাপ");৩টি ক্লিক [48, 208, 64] → INP 208: ৫০টির কম interaction, তাই সবচেয়ে ধীরটিই INP. ১২০টির ক্ষেত্রে সবচেয়ে ধীর চারটি 900, 640, 310, 149; ১২০ ÷ ৫০-এর পূর্ণ অংশ ২, তাই ওপরের দুটি বাদ আর INP 310, অর্থাৎ দুটি অদ্ভুত ঘটনা মাফ, তৃতীয়টি নয়। শেষ লাইন p75 INP = 120 ms → ভালো: ১২টির মধ্যে তিনটি page view ২০০ ms-এর ওপরে (২০৮, ২৩২, ২৮৮), তবু ৯ নম্বর মান ১২০, তাই পাতা “ভালো”। চারটি হলেই p75 সীমা পেরিয়ে যেত; p75 মানে প্রতি চারজনে একজনের খারাপ অভিজ্ঞতা লুকিয়ে থাকতে পারে।
Third-party script: chat, বিজ্ঞাপন আর analytics
Third-party script-এর কোড আপনার নয়, কিন্তু চলে আপনার main thread-এ। অংশ ২-এর তিনটি অবস্থা: head-এ রাখলে আঁকা আটকায় (FCP ২.১ s), next/script-এর lazyOnload-এ সরালে আঁকা দ্রুত কিন্তু TBT ১,১১০ ms আর “Max Potential First Input Delay” ১,২০০ ms (ক্রেতা ঠিক তখন ক্লিক করলে এতক্ষণ অপেক্ষা), আর facade-এ, অর্থাৎ শুরুতে শুধু একটি হালকা “চ্যাটে প্রশ্ন করুন” বোতাম আর ক্লিকের পরে আসল script, TBT ৪০ ms. next/script-এর strategy শুধু সময় বদলায়; আসল প্রশ্ন প্রতিটি script-এর লাভ, খরচ আর মালিক। নিরাপত্তার দিক (CSP, SRI) আছে পর্ব ৫০-এ।
৭ · HTTP cache header বাস্তবে — Cache-Control, ETag আর 304
সবচেয়ে দ্রুত অনুরোধ হলো যেটি নেটওয়ার্কে যায়ই না। মুদি দোকানের কথা ভাবুন: চাল, ডাল, লবণ তাকে রাখা থাকে, প্রতিবার গুদামে যেতে হয় না; কিন্তু আজকের মাছের দাম দোকানি ফোন করে জেনে নেন। কোন জিনিস কতদিন তাকে রাখা যাবে, সেটা বলে Cache-Control header. মূল directive-গুলো:
| Directive | মানে | বাজারঘরে কোথায় |
|---|---|---|
max-age=N | N সেকেন্ড টাটকা; এর মধ্যে জিজ্ঞেস না করেই ব্যবহার | JS, CSS, ছবি |
immutable | টাটকা থাকা পর্যন্ত reload-এও যাচাই নয় | নামে hash থাকা ফাইল |
no-cache | রাখা যাবে, কিন্তু প্রতিবার যাচাই (ETag বা Last-Modified দিয়ে) | HTML |
no-store | কোথাও রাখা যাবে না | কার্ট, অর্ডার, ব্যক্তিগত তথ্য |
private / public | শুধু browser রাখবে / CDN আর proxy-ও রাখতে পারে | ব্যক্তিগত বনাম সবার জন্য এক |
s-maxage=N | শুধু shared cache-এর (CDN) জন্য আলাদা মেয়াদ | ISR পাতা |
stale-while-revalidate=N | মেয়াদ শেষেও N সেকেন্ড পুরোনো কপি দেখিয়ে পেছনে নতুন আনা | দামের API |
সিদ্ধান্তের যুক্তিটা ছোট। নামে content hash থাকা ফাইল (130hk7hhcneuu.js) কখনো বদলায় না; বদলালে নামই বদলায়। তাই এক বছর আর immutable. HTML সেই নামগুলোর তালিকা ধরে রাখে, তাই HTML-কে দীর্ঘ cache দিলে নতুন deploy-এর পরও ক্রেতা পুরোনো JS-এর নাম পাবেন। তাই HTML-এ no-cache বা খুব ছোট মেয়াদ, আর ETag দিয়ে সস্তা যাচাই। নিচের ব্লক বাজারঘরের নিয়মগুলো একটি function-এ লেখে:
// বাজারঘরের নিয়ম: কোন ফাইলে কোন Cache-Control
const YEAR = 31536000;
function cacheControl(path, { personal = false } = {}) {
if (personal) return "private, no-store"; // কার্ট, অর্ডার, প্রোফাইল
if (/^\/_next\/static\//.test(path) || /\.[0-9a-z_-]{8,}\.(js|css|woff2|avif|webp)$/.test(path))
return `public, max-age=${YEAR}, immutable`; // নামে hash: কখনো বদলায় না
if (path.startsWith("/api/"))
return "public, max-age=30, stale-while-revalidate=300"; // দাম: আধা মিনিট টাটকা
if (/\.(jpg|png|svg|ico)$/.test(path))
return "public, max-age=86400, stale-while-revalidate=604800"; // hash ছাড়া ছবি: এক দিন
return "no-cache"; // HTML: প্রতিবার ETag যাচাই
}
const paths = [
["/_next/static/chunks/130hk7hhcneuu.js"],
["/fonts/bn.2imrbn8bck5ne.woff2"],
["/product/ilish"],
["/api/products/ilish-1kg/price"],
["/api/cart", { personal: true }],
["/ilish-photo.jpg"],
["/fonts/bn.woff2"],
];
for (const [p, o] of paths) console.log(p.padEnd(36), "→", cacheControl(p, o));
// একজন ক্রেতা এক সপ্তাহে ১০ বার পাতা খুললে একটি JS chunk কত বাইট নামায়?
const visits = 10, size = 74463; // gzip-এ বাইট
const hashed = 1 * size, unhashed = visits * size; // max-age=0 হলে প্রতিবার
console.log(`hash+immutable: ${hashed} বাইট, max-age=0 (304 ছাড়া): ${unhashed} বাইট`);প্রথম দুই লাইন hash নামের ফাইল: public, max-age=31536000, immutable. পাতা /product/ilish পেল no-cache, দামের API public, max-age=30, stale-while-revalidate=300, কার্ট private, no-store, hash ছাড়া ছবি এক দিন। শেষ path /fonts/bn.woff2-এ hash নেই, তাই নিয়ম তাকে no-cache-এ ফেলেছে; ইচ্ছাকৃত, কারণ নাম না বদলালে দীর্ঘ cache বিপজ্জনক। শেষ লাইন hash+immutable: 74463 বাইট, max-age=0 (304 ছাড়া): 744630 বাইট: দশবারে দশ গুণ। ETag থাকলে বাকি নয়বার body আসত না, কিন্তু নয়টি round trip তবু লাগত।
Next.js আসলে কী পাঠায় — আর reverse proxy কী বদলায়
next start-এর ওপর curl দিয়ে আসল header (সত্যিকারের run; Date, Connection আর লম্বা Vary বাদ):
$ curl -sD - -o /dev/null -H 'Accept-Encoding: gzip, br' http://127.0.0.1:48417/final/ilish
HTTP/1.1 200 OK
x-nextjs-cache: HIT
x-nextjs-prerender: 1
Cache-Control: s-maxage=31536000
ETag: "16koa96usqj9mq"
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
/_next/static/chunks/130hk7hhcneuu.js → Cache-Control: public, max-age=31536000, immutable
/_next/image?url=…&w=750&q=75 → Cache-Control: public, max-age=315360000, immutable
/ilish-photo.jpg (public/ folder) → Cache-Control: public, max-age=0
/api/price (route-এর নিজের header) → cache-control: public, max-age=30, stale-while-revalidate=300
তিনটি জিনিস চোখে পড়ার মতো। এক, static পাতার HTML-এ Next.js দিয়েছে s-maxage=31536000, অর্থাৎ CDN এক বছর রাখতে পারে আর browser-এর জন্য কোনো max-age নেই, তাই নতুন deploy-এর পরে CDN-এর কপি মোছা (purge) hosting-এর দায়িত্ব। ISR পাতায় stale-while-revalidate আসে (পর্ব ৫৫)। দুই, public/ folder-এর ফাইলের নামে hash নেই, তাই Next.js নিরাপদে max-age=0 দেয়। তিন, Next.js নিজে শুধু gzip করে, Brotli নয়।
Production-এ সাধারণত সামনে একটি reverse proxy থাকে। ল্যাবে Caddy 2.11.7 (GitHub release-এর SHA-512 checksum মিলিয়ে) 127.0.0.1:48418-এ বসানো হলো, compression আর header-এর নিয়মসহ:
{
admin off
}
http://127.0.0.1:48418 {
encode zstd gzip
@hashed path /_next/static/* /_next/image*
header @hashed >Cache-Control "public, max-age=31536000, immutable"
@html path /slow/* /fast/* /final/*
header @html >Cache-Control "no-cache"
@api path /api/*
header @api >Cache-Control "private, no-store"
reverse_proxy 127.0.0.1:48417 {
header_up -Accept-Encoding
}
}> চিহ্ন মানে header-টি upstream-এর উত্তর আসার পরে বসবে, তাই Next.js-এরটা ঢেকে দেবে। header_up -Accept-Encoding Next.js-কে সংকোচন ছাড়া পাঠাতে বলে, যাতে proxy নিজে সংকোচন করে (Next.js-এ compress: false দেওয়াও একই উদ্দেশ্যে)। Proxy-র ভেতর দিয়ে আসল header:
$ curl -sD - -o /dev/null -H 'Accept-Encoding: zstd, gzip' http://127.0.0.1:48418/final/ilish
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Encoding: zstd
Etag: "16koa96usqj9mq-zstd"
Via: 1.1 Caddy
…
/api/price → Cache-Control: private, no-store
দুটি শিক্ষা। Caddy ETag-এর শেষে -zstd জুড়ে দিয়েছে, কারণ সংকুচিত body আর মূল body আলাদা byte, তাই একই ETag রাখা ভুল হতো (Vary: Accept-Encoding-ও একই কারণে)। আর /api/price-এর route নিজে যে public, max-age=30, stale-while-revalidate=300 দিয়েছিল, proxy-র সাধারণ /api/* নিয়ম তা চুপচাপ মুছে দিয়েছে। Header একটি জায়গায় ঠিক করুন।
ETag আর 304: পুরোনো কপি এখনো ঠিক আছে কি না
HTML-এ no-cache মানে “প্রতিবার জিজ্ঞেস করুন”, কিন্তু জিজ্ঞেস করা সস্তা হতে পারে। Browser আগের উত্তরের ETag If-None-Match-এ পাঠায়; কিছু না বদলালে server body ছাড়া 304 Not Modified ফেরায়। এটি একটি conditional request; status code আর ETag-এর পুরো আলোচনা পর্ব ৩৩-এ। নিচের stepper ল্যাবের আসল header দিয়ে:
- Browser
GETপ্রথম visit - Server
200 + ETag৮৩১ KB - Browser cache
max-age=0সাথে সাথে বাসি - If-None-Match
W/"cae3c-…"দ্বিতীয় visit - 304
body নেইপুরোনো কপি চলবে
GET /ilish-photo.jpg
HTTP/1.1 200 OK
Cache-Control: public, max-age=0
Last-Modified: Tue, 03 Nov 2026 11:16:09 GMT
ETag: W/"cae3c-1a1a11faa29"
Content-Length: 831036
key: GET http://…/ilish-photo.jpg
body: 831036 B
etag: W/"cae3c-1a1a11faa29"
fresh: 0 s (max-age=0) → ব্যবহারের আগে যাচাই
GET /ilish-photo.jpg
If-None-Match: W/"cae3c-1a1a11faa29"
(ETag-এর প্রথম অংশ cae3c = 831036 hex-এ, ফাইলের আকার)
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=0
ETag: W/"cae3c-1a1a11faa29"
(body: 0 B, তবু একটি round trip)
/_next/static/chunks/130hk7hhcneuu.js
Cache-Control: public, max-age=31536000, immutable
দ্বিতীয় visit: কোনো অনুরোধই নেই, 0 RTT
HTML (/final/ilish, ETag "16koa96usqj9mq") → 304 একইভাবে
- প্রথম visit. Browser ছবি চাইল, server পুরো ৮৩১,০৩৬ byte দিল, সাথে একটি weak ETag (
W/দিয়ে শুরু) আরmax-age=0. এটিpublic/folder-এর ফাইল, নামে hash নেই। - Browser cache-এ রাখা। Body আর ETag রাখা হলো, কিন্তু
max-age=0মানে টাটকা থাকার সময় শূন্য। পরের বার ব্যবহারের আগে server-কে জিজ্ঞেস করতেই হবে। - দ্বিতীয় visit. Browser ETag-টি
If-None-Matchheader-এ পাঠাল: “আমার কাছে এই সংস্করণ আছে, বদলেছে কি?” এখানে weak ETag-এরcae3chex-এ ৮৩১,০৩৬, ফাইলের আকার। - 304 Not Modified. মিলে গেছে, তাই server body ছাড়াই উত্তর দিল। ৮৩১ KB-র বদলে কয়েকশো byte-এর header; কিন্তু ধীর নেটওয়ার্কে একটি round trip (মডেলে ১৫০ ms) তবুও লাগে। ল্যাবে curl দিয়ে ঠিক এই 304 পাওয়া গেছে।
- Hash নামের সাথে তুলনা।
immutableআর এক বছরের chunk-এ browser কিছুই জিজ্ঞেস করে না: শূন্য round trip. HTML-এর ETag দিয়েও 304 পাওয়া গেছে, যেটাno-cacheHTML-এর স্বাভাবিক পথ। তাই নিয়ম: যা বদলাতে পারে তার জন্য 304, যা বদলায় না তার জন্য hash আর immutable.
৮ · Compression, HTTP/2–3 আর আগাম সংযোগ
লেখা-ভিত্তিক ফাইল (HTML, CSS, JS, JSON, SVG) খুব ভালো সংকুচিত হয়, কারণ এতে একই শব্দ বারবার আসে। Browser Accept-Encoding-এ জানায় সে কী বোঝে, server Content-Encoding-এ জানায় কী দিল। তিনটি পদ্ধতি এখন সাধারণ: gzip (সবাই বোঝে), Brotli (br, সাধারণত আরও ছোট) আর zstd (দ্রুত, নতুন browser-এ)। নিচের ব্লক node:zlib দিয়ে একই ডেটা তিনভাবে সংকুচিত করে, বাংলা লেখাসহ:
// gzip বনাম Brotli: একই লেখা, ভিন্ন সংকোচন (node:zlib)
const zlib = require("node:zlib");
const Q = zlib.constants.BROTLI_PARAM_QUALITY;
const card = (i) =>
`<li class="card"><a href="/product/p${i}"><img src="/img/p${i}.avif" width="320" height="240" alt="পণ্য ${i}">` +
`<h3>পদ্মার ইলিশ (১ কেজি) — ধরন ${i}</h3><p class="price">৳ ${1200 + i * 5}</p></a></li>`;
const html = "<ul>" + Array.from({ length: 40 }, (_, i) => card(i)).join("\n") + "</ul>";
const bangla = ["পদ্মার তাজা ইলিশ, ধরার দিনই বরফে মোড়ানো।", "প্রতিটি মাছ ৯০০ গ্রাম থেকে ১.১ কেজি।",
"ঢাকার ভেতরে একই দিনে ডেলিভারি, অন্য জেলায় এক থেকে দুই দিন।", "কাটা ও ধোয়া চাইলে অর্ডারের সময় জানান।",
"মাছ পৌঁছানোর পর তিন ঘণ্টার মধ্যে ফ্রিজে রাখুন।", "চিনিগুঁড়া চাল আর মসুর ডালে আজ বিশেষ ছাড়।"].join(" ").repeat(4);
const json = JSON.stringify(Array.from({ length: 200 }, (_, i) => ({ id: i, sku: `sku-${i}`, price: 100 + (i % 37), stock: i % 9 })));
function row(name, text) {
const buf = Buffer.isBuffer(text) ? text : Buffer.from(text, "utf8");
const g = zlib.gzipSync(buf, { level: 6 }).length;
const b5 = zlib.brotliCompressSync(buf, { params: { [Q]: 5 } }).length;
const b11 = zlib.brotliCompressSync(buf, { params: { [Q]: 11 } }).length;
const pct = (n) => ((100 * n) / buf.length).toFixed(1) + "%";
console.log(`${name.padEnd(12)} raw ${String(buf.length).padStart(6)} gzip-6 ${String(g).padStart(5)} (${pct(g)})` +
` br-5 ${String(b5).padStart(5)} (${pct(b5)}) br-11 ${String(b11).padStart(5)} (${pct(b11)})`);
}
row("HTML list", html);
row("Bangla text", bangla);
row("JSON", json);
console.log("বাংলার এক অক্ষর UTF-8-এ", Buffer.byteLength("ই"), "বাইট; ইংরেজি 'e'", Buffer.byteLength("e"), "বাইট");
let s = 7; // আগে থেকেই সংকুচিত ফাইলের (AVIF, JPEG) মতো এলোমেলো বাইট
const avifLike = Buffer.from(Array.from({ length: 4000 }, () => (s = (s * 16807) % 2147483647) & 255));
row("AVIF-like", avifLike);Wandbox-এর Node 20-এ আসল আউটপুট: HTML তালিকা ৯,০৪৮ byte থেকে gzip-এ 781 (8.6%), Brotli q11-এ 459 (5.1%). বাংলা লেখা ২,৮১৬ byte থেকে gzip 345 (12.3%), Brotli 296 (10.5%); বাংলায় ব্যবধান কম, সম্ভবত কারণ Brotli-র তৈরি অভিধানে মূলত ইংরেজি আর HTML-এর শব্দ। JSON-এ 1547 (16.1%) বনাম 780 (8.1%). বাংলার এক অক্ষর UTF-8-এ 3 বাইট, তাই বাংলা পাতার কাঁচা আকার বড়, compression না দিলে ক্ষতি বেশি। শেষ লাইন: এলোমেলো byte gzip-এ 4023 (100.6%), অর্থাৎ বরং বড় হলো; AVIF বা JPEG-এ সংকোচন বৃথা।
বাজারঘরের আসল build-এ একই তুলনা (সত্যিকারের run; প্রথম দুটি সারি Caddy-র ভেতর দিয়ে curl-এর size_download, শেষটি একই ফাইলে node:zlib, সব byte-এ):
| ফাইল | কাঁচা | gzip (Caddy) | zstd (Caddy) | Brotli q11 (node) |
|---|---|---|---|---|
| পণ্যের পাতার HTML | 13,550 | 3,967 | 4,096 | 3,264 |
| সবচেয়ে বড় JS chunk | 228,093 | 74,463 | 74,624 | 61,095 |
Brotli-র সর্বোচ্চ মাত্রা (q11) এখানে gzip-এর চেয়ে প্রায় ১৮% ছোট (৬১,০৯৫ বনাম ৭৪,৪৬৩), কিন্তু q11 ধীর, তাই প্রতি অনুরোধে চালানো হয় না। সাধারণ চর্চা: build-এর সময় static ফাইলের .br আগেই বানিয়ে রাখা, আর dynamic HTML-এ মাঝারি মাত্রা। Caddy-র মূল build-এ encode gzip আর zstd জানে, Brotli নয় (plugin লাগে); nginx-এও Brotli আলাদা module. অনেক CDN নিজে Brotli করে; আপনার hosting কী করে, তাদের docs দেখে নিন। আর ছবি, video, woff2 আগে থেকেই সংকুচিত, এগুলোতে gzip দেওয়া শুধু CPU নষ্ট।
HTTP/2, HTTP/3 — সংক্ষেপে
ল্যাবের Lighthouse network তালিকায় প্রতিটি অনুরোধের protocol ছিল http/1.1, কারণ localhost-এ HTTPS ছিল না। Production-এ HTTPS-এর ওপর HTTP/2 প্রায় সবখানে চালু থাকে: একটি সংযোগে অনেক অনুরোধ একসাথে (multiplexing), header সংকোচন। তাই HTTP/1.1-এর যুগের কৌশল (সব JS এক ফাইলে, ছবি কয়েকটি domain-এ) এখন প্রায়ই ক্ষতিকর। HTTP/3 চলে QUIC-এর ওপর (UDP), একটি packet হারালে বাকি stream আটকে থাকে না, আর নেটওয়ার্ক বদলালে (Wi-Fi থেকে মোবাইল ডেটা) সংযোগ টিকে থাকতে পারে; দুর্বল মোবাইল নেটওয়ার্কে এর সুবিধা বেশি হওয়ার কথা। তত্ত্ব আছে Computer Networks-এর HTTP/2 আর HTTP/3 পর্বে।
preconnect আর dns-prefetch
ছবি যদি অন্য domain থেকে আসে (ধরুন img.bazarghar.example-এর CDN), প্রথম ছবির আগে সেই domain-এর DNS, TCP আর TLS লাগে, অংশ ৪-এর মডেলে তিন round trip, ৪৫০ ms. preconnect সংযোগটা আগেই খুলে রাখে; dns-prefetch শুধু DNS:
<link rel="preconnect" href="https://img.bazarghar.example" crossorigin>
<link rel="dns-prefetch" href="https://analytics.example">React 19-এ react-dom থেকে preconnect(), prefetchDNS(), preload() আর preinit() function আছে, যেগুলো component থেকে ডাকলে React head-এ এই tag বসায়। সাবধান: preconnect শুধু দু-একটি জরুরি origin-এ; বেশি দিলে অকেজো সংযোগে সময় নষ্ট।
৯ · ধীর নেটওয়ার্ক, কম দামের ফোন আর performance budget
বাংলাদেশে অনেক ক্রেতা মোবাইল ডেটায় আর কম বা মাঝারি দামের Android ফোনে কেনাকাটা করেন, আর শহরের বাইরে বা ভিড়ের সময়ে নেটওয়ার্ক প্রায়ই দুর্বল থাকে; এটি সাধারণ অভিজ্ঞতা, এখানে কোনো সংখ্যা দাবি করা হচ্ছে না। দুটি জিনিস এখানে বিশেষ। এক, prepaid ডেটায় প্রতিটি megabyte-এর দাম ক্রেতা নিজে দেন, তাই ৮৩১ KB-র একটি ছবি শুধু সময় নয়, টাকাও। দুই, সস্তা ফোনের CPU ধীর, তাই JavaScript-এর খরচ সেখানে কয়েকগুণ; Lighthouse-এর ৪ গুণ ধীর CPU সেই কারণেই। একটি সহজ হিসাব: ১ MB (১০ লাখ byte) ১.৬৩৮৪ Mbps-এ নামতে ১০,০০,০০০ × ৮ ÷ ১৬,৩৮,৪০০ ≈ ৪,৮৮৩ ms, LCP-র পুরো ২.৫ সেকেন্ডের প্রায় দ্বিগুণ।
তাই সীমা আগে ঠিক করুন, পরে নয়। Performance budget মানে কয়েকটি সংখ্যা, যা পেরোলে build লাল হবে: যেমন মোবাইল lab-এ LCP ≤ ২,৫০০ ms, CLS ≤ ০.১, TBT ≤ ২০০ ms, ছবির মোট ≤ ১৫০ KB, JavaScript ≤ ২০০ KB (সংকুচিত)। এগুলো বাজারঘরের ধরে নেওয়া সংখ্যা, সবার জন্য নিয়ম নয়। Lighthouse CI দিয়ে লেখা config:
{
"ci": {
"collect": {
"url": ["http://127.0.0.1:48417/slow/ilish", "http://127.0.0.1:48417/final/ilish"],
"numberOfRuns": 3,
"settings": { "onlyCategories": ["performance"], "chromeFlags": "--headless=new --no-first-run" }
},
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"total-blocking-time": ["error", { "maxNumericValue": 200 }],
"resource-summary:script:size": ["warn", { "maxNumericValue": 200000 }],
"resource-summary:image:size": ["error", { "maxNumericValue": 150000 }]
}
}
}
}CHROME_PATH-এ Edge দিয়ে lhci collect তারপর lhci assert-এর আসল আউটপুট (রং-এর code বাদ, কিছু link ছাঁটা):
$ npx lhci collect && npx lhci assert
Running Lighthouse 3 time(s) on http://127.0.0.1:48417/slow/ilish
Run #1...done. Run #2...done. Run #3...done.
Running Lighthouse 3 time(s) on http://127.0.0.1:48417/final/ilish
Run #1...done. Run #2...done. Run #3...done.
Checking assertions against 2 URL(s), 6 total run(s)
4 result(s) for http://127.0.0.1:48417/slow/ilish :
× categories.performance failure for minScore assertion
expected: >=0.9 found: 0.87 all values: 0.67, 0.67, 0.87
× cumulative-layout-shift failure for maxNumericValue assertion
expected: <=0.1 found: 0.177557268171308
× largest-contentful-paint failure for maxNumericValue assertion
expected: <=2500 found: 2555.9922 all values: 6453.7733, 6453.554, 2555.9922
× resource-summary.image.size failure for maxNumericValue assertion
expected: <=150000 found: 831320
Assertion failed. Exiting with status code 1.
/final/ilish-এর কোনো ব্যর্থতা নেই, আর exit code 1 মানে CI-তে এই job লাল হবে। একটি সূক্ষ্ম বিষয় দেখুন: “found: 0.87” তিনটির সবচেয়ে ভালোটি। Lighthouse CI ডিফল্টে আশাবাদী (optimistic) হিসাব নেয়, অর্থাৎ তিনবারের সেরাটি দিয়ে বিচার করে, যাতে একটি এলোমেলো খারাপ run build ভাঙাতে না পারে; তবু এখানে সেরাটিও ব্যর্থ। Pipeline-এ কোথায় বসবে (build-এর পরে, preview deploy-এর ওপর, নাকি PR-এ মন্তব্য হিসেবে), সেটি পর্ব ৮১-এর (CI/CD) বিষয়; pipeline নকশার তত্ত্ব Cloud ও DevOps-এর pipeline design পর্বে। CI-এর যন্ত্রে মাপ বেশি দোলে, তাই সীমায় একটু ফাঁক রাখুন।
শেষ চেকলিস্ট — একটি পাতা production-এ পাঠানোর আগে
আরও গভীরে। Lighthouse-এর simulation “Lantern” নামের একটি মডেল: আসল run থেকে অনুরোধগুলোর নির্ভরতার গ্রাফ বানিয়ে ধীর নেটওয়ার্ক আর CPU-তে সেই গ্রাফ আবার “খেলে” দেখে, একটি আশাবাদী আর একটি নিরাশাবাদী অনুমান মিলিয়ে। LCP আর INP-এর ভিত্তি W3C-র Web Performance Working Group-এর spec: Largest Contentful Paint, Layout Instability আর Event Timing API. HTTP caching-এর নিয়ম আছে RFC 9111-এ, immutable RFC 8246-এ (২০১৭), stale-while-revalidate RFC 5861-এ (২০১০)। Brotli RFC 7932 (২০১৬), zstd RFC 8878 (২০২১), HTTP/3 RFC 9114 (২০২২)।
পরীক্ষা ও ইন্টারভিউতে। (১) LCP, INP, CLS কী মাপে আর ভালোর সীমা কত? লোড, সাড়া, স্থিরতা; ২.৫ s, ২০০ ms, ০.১, আসল ব্যবহারকারীর p75. (২) Lab আর field data-র পার্থক্য? Lab নিয়ন্ত্রিত পরিবেশে একটি যন্ত্রে, ভুল খুঁজতে; field আসল ব্যবহারকারীর, রায়ের জন্য; lab-এ INP নেই, TBT তার কাছাকাছি সংকেত। (৩) নামে hash থাকা JS-এ কোন Cache-Control? public, max-age=31536000, immutable; HTML-এ no-cache আর ETag. (৪) ছবি থেকে CLS কীভাবে ঠেকাবেন? width আর height বা aspect-ratio দিয়ে আগে জায়গা রাখা। (৫) no-cache আর no-store-এর পার্থক্য? প্রথমটি রাখে কিন্তু প্রতিবার যাচাই করে, দ্বিতীয়টি রাখেই না।
১০ · AI-এর যুগে web performance
“সাইট দ্রুত করে দাও” বললে AI সাধারণত লম্বা, সাধারণ আর কিছুটা পুরোনো তালিকা দেয়। Performance-এর কাজ শুরু হয় মাপ থেকে, তাই AI-কেও মাপ দিন। আর তার দেওয়া প্রতিটি পরামর্শ আবার মাপুন: অংশ ২-এ “প্রথম চেষ্টা” পাতা ঠিক এমন একটি সাধারণ পরামর্শ (“third-party script lazy করুন”) মানার ফল, যেটি TBT প্রায় ২৮ গুণ বাড়িয়েছিল।
AI-কে প্রশ্ন করার আগে মাপ জোগাড় করুন: কোন পাতা, কোন version, Lighthouse-এর কোন audit ব্যর্থ, field data-য় কোন মাপ খারাপ। তারপর এমন কাঠামোয় জিজ্ঞেস করুন:
AI-এর performance পরামর্শ মেলানোর চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১FID তো ভালোই চলছিল; INP আনার দরকার পড়ল কেন, আর এতে সাইটগুলোর কাজ কীভাবে বদলাল?
২০২০ সালে যখন Core Web Vitals আসে, তখন সাড়া দেওয়ার মাপ ছিল FID: পাতায় প্রথম ক্লিক বা ট্যাপের পরে handler শুরু হতে কত অপেক্ষা। দুটি বড় ফাঁক ছিল। প্রথমত, শুধু প্রথম interaction গোনা হতো; একটি দোকানের পাতায় ক্রেতা হয়তো বিশবার ক্লিক করেন, আর সবচেয়ে ধীরটি প্রায়ই প্রথমটি নয় (যেমন filter বদলানো, কার্টে যোগ)। দ্বিতীয়ত, শুধু input delay মাপা হতো; handler নিজে ৫০০ ms চললে বা তার পরে React-এর বড় render আর layout-এ আঁকা আটকে থাকলে FID তা দেখত না। ফলে অনেক সাইট FID-এ “ভালো” থেকেও ক্রেতার কাছে আটকে যাওয়া মনে হতো।
Chrome দল Event Timing API দিয়ে প্রতিটি interaction-এর পুরো পথ মাপার ব্যবস্থা করে: input delay, processing, আর পরের ছবি আঁকা পর্যন্ত presentation delay. ২০২৩ সালে INP “pending” হয় আর ২০২৪ সালে (মার্চে) FID-এর জায়গা নেয়।
কাজের ধারাও বদলাল। আগে লক্ষ্য ছিল লোডের সময় main thread খালি রাখা; এখন পুরো visit জুড়ে প্রতিটি handler হালকা রাখতে হয়। এতে এসেছে কাজ ভাগ করা (scheduler.yield, setTimeout), React-এর transition, Web Worker, আর third-party script নিয়ে কড়া প্রশ্ন।
মূল উপলব্ধি: মাপ যা দেখে না, দল তা সারায় না। ভালো মাপ বেছে নেওয়া নিজেই একটি প্রকৌশল সিদ্ধান্ত, আর মাপ বদলালে অগ্রাধিকারও বদলায়।
প্র ০২Lighthouse-এ ৯৮ পেয়েছেন, অথচ ক্রেতারা বলছেন সাইট ধীর। কে ভুল, আর কীভাবে মীমাংসা করবেন?
সম্ভবত কেউই ভুল নয়; দুজন ভিন্ন প্রশ্নের উত্তর দিচ্ছেন। Lighthouse একটি নির্দিষ্ট নকল ফোনে, একটি নির্দিষ্ট নেটওয়ার্ক মডেলে, একটি নির্দিষ্ট URL-এ, একজন নতুন ভিজিটর হিসেবে, কোনো ক্লিক ছাড়া মাপে। ক্রেতারা আসেন নানা ফোন থেকে, অনেকে লগইন করা অবস্থায় (ব্যক্তিগত, cache হয় না এমন পাতা), অনেকে ফিল্টার আর খোঁজা ব্যবহার করে, কেউ ভিড়ের সময়ে দুর্বল সিগন্যালে। ল্যাবের পাতা হয়তো static product page, আর অভিযোগ আসছে checkout বা খোঁজার পাতা থেকে।
মীমাংসার পথ field data. প্রথমে web-vitals বা কোনো RUM ব্যবস্থা দিয়ে পাতা ধরে, যন্ত্রের ধরন ধরে, দেশ বা অঞ্চল ধরে LCP, INP, CLS-এর p75 দেখুন। attribution build (web-vitals/attribution) দিয়ে জানা যায় কোন element LCP, কোন interaction ধীর, কোন script দায়ী। তারপর সেই খারাপ অংশটি lab-এ পুনরায় তৈরি করুন: সেই পাতা, সেই ধরনের যন্ত্র (CPU throttling আরও বেশি), সেই কাজ (Playwright দিয়ে ক্লিক)। বাজারঘরের ল্যাবে localhost-এর “ক্রেতা” LCP ৭৪৪ ms পেয়েছিলেন, Lighthouse ৬.৫ s; দুটোই সত্য ছিল।
আরেকটি কারণ পরিসংখ্যানের। গড় বা median খারাপ অভিজ্ঞতা লুকায়; তাই p75, আর অনেক দল p90-ও দেখে। Google-এর CrUX-এর ডেটা মোটামুটি ২৮ দিনের চলমান সমষ্টি বলে পরিচিত, তাই সংশোধনের প্রভাব সেখানে দেরিতে দেখা যায়।
মূল উপলব্ধি: Lab বলে “কেন”, field বলে “কতজনের সাথে”। দুটোকে প্রতিদ্বন্দ্বী না ভেবে একটি থেকে প্রশ্ন নিয়ে অন্যটিতে উত্তর খুঁজুন।
প্র ০৩Brotli যদি gzip-এর চেয়ে ছোট করে, সব জায়গায় সর্বোচ্চ মাত্রায় Brotli চালাই না কেন? zstd কোথায় বসে?
Compression সবসময় একটি দর-কষাকষি: CPU-র সময় দিয়ে byte কেনা। Brotli Google ২০১৫ সালের দিকে প্রকাশ করে, আর ২০১৬ সালে RFC 7932 হয়; এর ভেতরে web-এর সাধারণ শব্দের (HTML tag, JavaScript keyword) একটি তৈরি অভিধান আছে, তাই ছোট ফাইলেও ভালো কাজ করে। কিন্তু সর্বোচ্চ মাত্রা (quality ১১) খুব ধীর। বাজারঘরের ২২৮ KB JS chunk-এ q11 gzip-এর চেয়ে প্রায় ১৮% ছোট করেছে; static ফাইল একবার build-এ সংকুচিত করে রাখলে এই খরচ একবারই, তাই সেখানে q11 যুক্তিসঙ্গত। কিন্তু প্রতি অনুরোধে বানানো HTML বা API-র JSON-এ q11 চালালে server-এর CPU-তে প্রতিটি অনুরোধের TTFB বাড়তে পারে, আর ভিড়ের সময় server আগে ক্লান্ত হয়। তাই dynamic উত্তরে সাধারণত মাঝারি মাত্রা (৪ থেকে ৬-এর মতো)।
zstd Facebook-এ (এখন Meta) তৈরি, ২০২১ সালে RFC 8878; এর বৈশিষ্ট্য খুব দ্রুত সংকোচন আর খোলা, একই গতিতে প্রায়ই gzip-এর চেয়ে ভালো অনুপাত। Chromium-ভিত্তিক browser আর Firefox ২০২৪ সালের দিক থেকে Content-Encoding: zstd বোঝে বলে জানা যায়, সব browser নয় (সঠিক version-এর জন্য caniuse দেখুন)। ল্যাবে Caddy-র zstd আকারে gzip-এর প্রায় সমান হয়েছে; এর সুবিধা গতিতে।
আর যা আগে থেকেই সংকুচিত (AVIF, JPEG, woff2, video), তাতে আবার সংকোচন বৃথা: ল্যাবের এলোমেলো byte gzip-এ বরং একটু বড় হয়েছে।
মূল উপলব্ধি: “সবচেয়ে ছোট” সবসময় “সবচেয়ে দ্রুত” নয়। কখন সংকুচিত হচ্ছে (build না অনুরোধে) আর কে খরচ দিচ্ছে (server না ক্রেতার নেটওয়ার্ক), এই দুই প্রশ্নে মাত্রা ঠিক হয়।
প্র ০৪ফাইলের নামে hash আর immutable কেন, app.js?v=2-এর মতো query string দিয়ে কি একই কাজ হয় না?
দুটোরই লক্ষ্য “cache busting”: পুরোনো ফাইল বদলালে browser যেন নতুনটা নেয়। Query string-এর পথ বহু পুরোনো, আর ABCL TECH-এর নিজের lesson.css-ও ?v=20261028 দিয়ে চলে। কিন্তু এর দুর্বলতা আছে। হাতে version বাড়াতে ভুলে গেলে ক্রেতা পুরোনো ফাইল পান; প্রতিটি deploy-এ সব ফাইলের version বাড়ালে বদলায়নি এমন ফাইলও আবার নামে।
Content hash এই সমস্যাগুলো সরায়: নাম আসে ফাইলের বিষয়বস্তু থেকে (130hk7hhcneuu.js), তাই বিষয় না বদলালে নামও বদলায় না, আর বদলালে নিজে থেকেই নতুন নাম। Bundler (Vite, Turbopack, webpack) এটি স্বয়ংক্রিয় করে, যা পর্ব ৭৯-এ দেখেছেন। এরপর এক বছর max-age নিরাপদ।
immutable কেন আলাদা? দীর্ঘ max-age থাকলেও ব্যবহারকারী reload চাপলে অনেক browser আগে প্রতিটি ফাইলের জন্য শর্তসহ অনুরোধ পাঠাত, যার প্রায় সবগুলোর উত্তর 304. Facebook-এর প্রকৌশলীরা ২০১৬ সালের দিকে দেখান যে এই অকারণ যাচাই তাদের static ফাইলের অনুরোধের বড় অংশ; সেখান থেকে immutable প্রস্তাব, ২০১৭ সালে RFC 8246. ফল একই: hash নামের ফাইলে শূন্য round trip.
কিন্তু HTML-এ এটি কখনো নয়। HTML-ই নতুন hash নামগুলো বহন করে; HTML পুরোনো থাকলে নতুন deploy কারো কাছে পৌঁছায় না। পুরোনো hash ফাইল deploy-এর পরে কিছুদিন রেখে দিন, খোলা tab সেগুলো চাইতে পারে।
মূল উপলব্ধি: যা কখনো বদলায় না তাকে চিরকাল cache করুন, আর যা বদলায় তার একটি ছোট, সস্তায় যাচাইযোগ্য “সূচিপত্র” রাখুন। এই দুই স্তরের নকশাই আধুনিক web caching-এর মূল।
প্র ০৫Chat widget, analytics, বিজ্ঞাপন — ব্যবসা চায়, performance ভোগে। দলের ভেতরে এই টানাপোড়েন কীভাবে মেটাবেন?
Third-party script-এর সমস্যা শুধু প্রযুক্তিগত নয়, মালিকানার। Marketing দল একটি tag manager-এ একটি নতুন script যোগ করল, কোনো code review হলো না, আর পরের সপ্তাহে INP খারাপ। ল্যাবের নকল widget মাত্র ৬১৯ byte, তবু ৩০০ ms main thread খেয়েছে; আকার দেখে খরচ বোঝা যায় না।
কাজের একটি কাঠামো: প্রতিটি third-party script-এর একটি তালিকা রাখুন, যাতে থাকে কে মালিক, কী ব্যবসায়িক কাজ, কোন পাতায় লাগে, আর মাপা খরচ (TBT, bytes). নতুন script যোগ মানে একটি PR আর Lighthouse CI-এর budget পার হওয়া। যেখানে সম্ভব facade (chat-এর বোতাম, video-র ছবি), ক্লিকের পরে লোড; যেখানে শুধু ডেটা পাঠাতে হয় (analytics), server-side পাঠানো বা হালকা beacon; বিজ্ঞাপনের জন্য জায়গা আগে রাখা, প্রথম পর্দার বাইরে থাকলে দেরিতে লোড।
আলোচনায় ভাষাটাও গুরুত্বপূর্ণ। “performance খারাপ হবে” বললে কেউ শোনে না; “এই widget যোগ করার পরে মোবাইলে কার্টে যোগের INP p75 ১৫০ থেকে ৩২০ ms, আর সেই সপ্তাহে কার্ট ছেড়ে যাওয়া এত বেড়েছে” বললে আলোচনা বদলায় (সংখ্যাগুলো এখানে কল্পিত উদাহরণ; নিজের ডেটা দিন)। আর প্রতিটি script-এর গোপনীয়তা ও নিরাপত্তার দিকও আছে, কারণ তারা পাতার সব ডেটা দেখতে পায়; এর জন্য পর্ব ৫০-এর CSP দেখুন।
মূল উপলব্ধি: Performance একটি ভাগাভাগির সম্পদ, যেমন রাস্তার জায়গা। প্রত্যেকে “আমার একটা ছোট্ট জিনিস” রাখলে রাস্তা আটকে যায়; তাই তালিকা, মালিক আর budget লাগে।
অনুশীলন
একটি হিসাব, একটি কমান্ডের কাজ, একটি নকশা পর্যালোচনা আর একটি ফল অনুমান।
-
হিসাব করুন: একটি পণ্যের পাতার hero ছবি ৫০০ KB (৫,০০,০০০ byte) JPEG. নেটওয়ার্ক ১.৬৩৮৪ Mbps, RTT ১৫০ ms, TTFB ৬১০ ms, আর ছবির অনুরোধ HTML আসার সাথে সাথেই যায়। (ক) শুধু ছবির জন্য LCP কত? (খ) একই ছবি ৪০ KB AVIF হলে? (গ) প্রথম অবস্থায় ছবি ৫০০ ms দেরিতে খোঁজা হলে (lazy) LCP কত, আর কোনটি “ভালো”?
(ক) নামার সময় = ৫,০০,০০০ × ৮ ÷ ১৬,৩৮,৪০০ ≈ ২,৪৪১ ms. LCP ≈ ৬১০ + ১৫০ + ২,৪৪১ = ৩,২০১ ms, “উন্নতি দরকার” (২.৫ থেকে ৪ s).
(খ) ৪০,০০০ × ৮ ÷ ১৬,৩৮,৪০০ ≈ ১৯৫ ms. LCP ≈ ৬১০ + ১৫০ + ১৯৫ = ৯৫৫ ms, “ভালো”।
(গ) ৩,২০১ + ৫০০ = ৩,৭০১ ms, তবুও “উন্নতি দরকার”, কিন্তু ৪ s-এর একদম কাছে। শুধু (খ) ভালো। শিক্ষা: ধীর নেটওয়ার্কে ছবির byte-ই সবচেয়ে বড় টুকরো; এই মডেলে render-blocking আর CPU ধরা হয়নি, বাস্তবে LCP আরও বেশি হবে।
-
কমান্ডের কাজ: নিজের যেকোনো Next.js project
next buildআরnext startকরে মোবাইল Lighthouse তিনবার চালান (Edge বা Chrome দিয়ে,--only-categories=performance). তারপর curl দিয়ে HTML, একটি/_next/staticফাইল আর একটিpublic/ফাইলেরCache-Controlপড়ুন। কী খুঁজবেন?তিন run-এর LCP, TBT, CLS পাশাপাশি লিখুন আর median নিন; একটি run বাকিদের থেকে অনেক আলাদা হলে কারণ খুঁজুন (lazy ছবি, দেরিতে আসা font). LCP element আর “LCP breakdown”-এর বড় টুকরোটি দেখুন। Header-এ সাধারণত দেখবেন
/_next/static-এpublic, max-age=31536000, immutable,public/ফাইলেpublic, max-age=0আর ETag, আর static HTML-এs-maxage=31536000বা ISR-এstale-while-revalidate. Proxy থাকলে তার ভেতর দিয়েও পড়ুন। -
নকশা পর্যালোচনা: একজন সহকর্মী nginx-এ লিখেছেন: সব
.jsআর.css-এmax-age=31536000, immutable(ফাইলের নামapp.js,style.css, hash নেই); HTML-এmax-age=86400;/api/cart-এpublic, max-age=60; আর ছবিতে gzip চালু। কী কী ভুল, কীভাবে সারাবেন?এক, hash ছাড়া নামে
immutableএক বছর: পরের deploy-এর পরে যাঁদের কাছে পুরোনোapp.jsআছে, তাঁরা এক বছর পুরোনো কোড চালাবেন। Bundler দিয়ে নামে content hash আনুন, তবেই immutable. দুই, HTML-এ এক দিন: নতুন deploy এক দিন পর্যন্ত অনেকে দেখবেন না;no-cacheআর ETag দিন। তিন, কার্ট ব্যক্তিগত, অথচpublic: একটি shared cache একজনের কার্ট আরেকজনকে দেখাতে পারে, এটি নিরাপত্তার ত্রুটি;private, no-storeদিন। চার, ছবিতে gzip: JPEG, AVIF আগে থেকেই সংকুচিত, শুধু CPU খরচ; gzip বা Brotli শুধু লেখা-ভিত্তিক ফাইলে। -
ফল অনুমান করুন: একটি পাতায় এই layout shift ঘটল (সময় ms, মান): ২০০ ms-এ ০.০৫, ৯০০ ms-এ ০.০৪, ২,৫০০ ms-এ ০.০৮, ৩,০০০ ms-এ ০.০৩, আর ৪,০০০ ms-এ ০.২০, যেটি একটি ক্লিকের ২০০ ms পরে। CLS কত, আর rating কী?
৪,০০০ ms-এর shift ক্লিকের ৫০০ ms-এর ভেতরে, তাই
hadRecentInput, গোনা হবে না। বাকিগুলো: ২০০ আর ৯০০-এর ফাঁক ৭০০ ms (< ১ s), তাই প্রথম window = ০.০৫ + ০.০৪ = ০.০৯. ৯০০ থেকে ২,৫০০ ফাঁক ১,৬০০ ms, নতুন window: ২,৫০০ আর ৩,০০০ (ফাঁক ৫০০ ms) = ০.০৮ + ০.০৩ = ০.১১. সবচেয়ে বড় window ০.১১, তাই CLS = ০.১১, rating “উন্নতি দরকার” (০.১-এর বেশি, ০.২৫-এর কম)। সব যোগ করলে ০.২০ হতো (ক্লিকেরটা বাদে), যা ভুল হিসাব।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি পণ্যের পাতা খোলার সময় ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- DNS, TCP আর TLS দিয়ে সংযোগ তৈরি
- HTML-এর প্রথম byte আসে (TTFB)
- Render-blocking CSS আর script নামে ও চলে
- প্রথম লেখা আঁকা হয় (FCP)
- Hero ছবি নেমে আঁকা হয় (LCP)
- ক্রেতা ক্লিক করেন আর পরের ছবি আঁকা হয় (INP-এর একটি interaction)
একটি পাতার আসল ব্যবহারকারীদের INP মানের 75th percentile ২৪০ ms. Core Web Vitals অনুযায়ী এটি কোন শ্রেণির?
- ভালো
- উন্নতি দরকার
- খারাপ
- বিচার করা যায় না, কারণ INP শুধু lab-এ মাপা যায়
একটি পাতার hero ছবিতে loading="lazy" দেওয়া, আর Lighthouse-এর LCP খুব বেশি। সবচেয়ে সরাসরি সংশোধন কোনটি?
- ছবিটি JavaScript দিয়ে পরে বসানো
- ছবিতে gzip চালু করা
- lazy সরিয়ে eager করা আর fetchpriority="high" দেওয়া
- ছবির ওপরে একটি min-height দেওয়া
একটি layout shift-এ সরে যাওয়া element-গুলো viewport-এর ৪০% জুড়েছিল (আগে আর পরে মিলিয়ে), আর সবচেয়ে বেশি সরেছে viewport-উচ্চতার ২৫%. এই shift-এর score কত?
- ০.১০
- ০.৬৫
- ০.৪০
- ০.২৫
Bundler-এর তৈরি main.4f9a2c1e.js ফাইলের জন্য কোন Cache-Control সবচেয়ে উপযুক্ত?
- no-store
- no-cache
- private, max-age=0
- public, max-age=31536000, immutable
একটি click handler-এ ২০০ ms-এর হিসাব চলে, তারপর UI বদলায়। INP কমানোর সবচেয়ে কার্যকর পরিবর্তন কোনটি?
- হিসাবটি একটি আলাদা function-এ সরানো, একই জায়গা থেকে ডাকা
- আগে UI বদলানো, তারপর scheduler.yield() বা setTimeout দিয়ে বাকি হিসাব টুকরো করে চালানো
- পাতায় preconnect যোগ করা
- ছবিগুলো AVIF-এ বদলানো
একজন ক্রেতার browser-এ ETag: "abc" সহ একটি HTML রাখা আছে, Cache-Control no-cache. পাতা না বদলালে আবার খোলার সময় কী হয়?
- Browser পুরো HTML আবার নামায়, ২০০ সহ
- Browser কিছু না জিজ্ঞেস করেই রাখা কপি দেখায়
- Browser If-None-Match পাঠায়, server 304 দেয়, রাখা কপি ব্যবহার হয়
- Server 412 Precondition Failed দেয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- LCP ≤ ২.৫ s, INP ≤ ২০০ ms, CLS ≤ ০.১, আসল ব্যবহারকারীদের p75 ধরে; lab (Lighthouse) কারণ খোঁজে, field (
web-vitals) রায় দেয়। - Production build-এ মাপুন, কয়েকবার চালিয়ে median নিন; একটি সংশোধন অন্য মাপ খারাপ করছে কি না দেখুন।
- LCP: hero ছবিতে lazy নয়,
fetchpriority="high", ঠিক মাপ আর AVIF বা WebP, render-blocking কম। CLS: আগে জায়গা রাখা। INP: আগে UI, তারপর কাজ ভাগ, third-party script চাইলে তবে। - Hash নামে
immutableএক বছর, HTML-এno-cacheআর 304, ব্যক্তিগত তথ্যেprivate, no-store; header এক জায়গায় ঠিক করুন। - লেখা ফাইলে gzip বা Brotli, ছবিতে নয়; আর budget-কে CI-তে বসান, যাতে ধীর পাতা merge না হয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৮৫ · Stack বাছাই — MERN, Next.js, Django, Laravel আর সিদ্ধান্তের ছকপরবর্তী পর্বসব শেখা এক জায়গায়: কোন প্রকল্পে কোন stack, সিদ্ধান্তের ছক দিয়ে।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের রোডম্যাপ; পরের পর্ব ৮৫ · Stack বাছাই।
- পর্ব ২৩ · React performanceপেছনে ফিরেmemo, transition, code splitting আর INP-এর শুরু।
- পর্ব ৫৫ · SSR, SSG, ISRপেছনে ফিরেTTFB, CDN-এর header আর s-maxage.
- পর্ব ৬৪ · File storage আর image pipelineপেছনে ফিরেsharp, srcset আর next/image-এর loader.
- HTML ও CSS · Performanceপাশের ট্র্যাকCritical CSS, ছবি আর font-এর ভিত্তি।
- Computer Networks · Web caching আর CDNগভীরে যানHTTP caching-এর নিয়ম আর CDN-এর ভেতর।
- System Design · CDN আর edgeগভীরে যানবড় মাপে cache-এর নকশা।