সরাসরি পর্বে যান
পর্ব ৫৫ · ৯৩-এর মধ্যে · মডিউল ৮

SSR, SSG, ISR আর partial prerendering — কখন কোনটি

SSR, SSG, ISR and Partial Prerendering in Next.js — When to Use Which

Taught in Bangla, this lesson shows how Next.js picks a rendering strategy from the APIs your code touches and how the build route table records it, static generation with generateStaticParams, what makes a page dynamic, ISR with a real stale-then-fresh run, partial prerendering with cache components in Next.js 16, Edge versus Node runtime, static export for cheap hosting and its limits, measuring TTFB and CDN headers, and per-page choices.

এই পর্বে যা শিখবেন

  • generateStaticParams দিয়ে build-এর সময় পণ্যের পাতা বানানো আর build output পড়া
  • কোন API একটি route-কে প্রতি অনুরোধে চালাতে বাধ্য করে, আর তা কীভাবে যাচাই করবেন
  • সময়ভিত্তিক আর on-demand ISR, সত্যিকারের timestamp আর x-nextjs-cache header দিয়ে
  • Partial prerendering: static shell আর stream হয়ে আসা dynamic গর্ত, cacheComponents-সহ
  • Static export, edge বনাম Node runtime, curl দিয়ে TTFB মাপা, আর “বাজারঘর”-এর প্রতিটি পাতার কৌশল

দামের একক নিয়ে: এই পর্বের ছোট ল্যাবে উদাহরণ সহজ রাখতে দাম পূর্ণ টাকায় রাখা হয়েছে, আর আউটপুটগুলোও সেই run-এর। আসল দোকানে টাকা রাখুন পয়সায়, পূর্ণসংখ্যা হিসেবে, যেমনটি পর্ব ৩২-এ দেখেছেন।

১ · মানচিত্র থেকে মাঠে: Next.js কীভাবে কৌশল ঠিক করে

পর্ব ০৩-এ আপনি rendering-এর মানচিত্র দেখেছেন। সেখানে প্রশ্ন ছিল দুটি: HTML কোথায় তৈরি হয়, আর কখন। এবার একই প্রশ্ন Next.js-এর ভেতরে, হাতে-কলমে। Routing আর layout পর্ব ৫১-এ, Server Component পর্ব ৫২-এ, আর data fetching ও cache পর্ব ৫৩-এ আলাদা করে আসছে। এই পর্বের কাজ হলো পুরো পাতার কৌশল বেছে নেওয়া।

প্রথম বড় কথা: Next.js-এ আপনি সাধারণত “এই পাতা SSR হবে” বলে কোনো সুইচ টেপেন না। Framework আপনার কোড পড়ে সিদ্ধান্ত নেয়। কোনো component যদি cookies() পড়ে, তাহলে উত্তর নির্ভর করে কে অনুরোধ করছেন তার ওপর, তাই পাতাটি আগে থেকে বানানো যায় না। আর যদি কিছুই অনুরোধের ওপর নির্ভর না করে, Next.js পাতাটি build time-এ বানিয়ে রাখে। এই আগে থেকে বানানোকে বলে prerender.

পুরান ঢাকার একটি বিরিয়ানির দোকান ভাবুন। সকালে বড় ডেকচিতে কাচ্চি রান্না হয়, দুপুরে চাইলেই প্লেটে পান: এটি SSG. কেউ “ঝাল কম, আলু বেশি” চাইলে আলাদা করে বানাতে হয়: এটি প্রতি অনুরোধের rendering, বা SSR. ডেকচির খাবার দুই ঘণ্টা পর ঠান্ডা হলে নতুন ডেকচি চড়ে, কিন্তু ততক্ষণ পুরোনোটিই পরিবেশন হয়: এটি ISR. আর প্লেটে আগে পোলাও আর মাংস দিয়ে দেওয়া হলো, বোরহানি পরে টেবিলে এল: এটি partial prerendering. দোকানি প্রতিটি অর্ডারের ধরন দেখে ঠিক করেন কোনটি কীভাবে যাবে, Next.js-ও তাই করে।

Next.js 16-এর দুটি মডেল

লেখার সময়ে (নভেম্বর ২০২৬) npm-এ Next.js-এর স্থিতিশীল সংস্করণ 16.4.4. এই সংস্করণে rendering আর cache-এর দুটি মডেল পাশাপাশি আছে, আর পুরোনো টিউটোরিয়াল পড়তে গিয়ে এখানেই সবচেয়ে বেশি গোলমাল হয়:

বিষয়আগের মডেল (cacheComponents বন্ধ)Cache Components মডেল
ডিফল্টযা পারা যায় static; একটি cookies() পুরো route-কে dynamic করেডেটা আনা ডিফল্টে প্রতি অনুরোধে; cache করতে হয় স্পষ্ট করে
ISR লেখা হয়export const revalidate = 60'use cache' আর cacheLife(...)
এক পাতায় static আর dynamicনা; পুরো route এক ধরনেরহ্যাঁ; এটিই partial prerendering
Build table-এর চিহ্ন○ ● ƒ○ ◐
নতুন প্রজেক্টেহাতে বানানো next.config বা পুরোনো প্রজেক্টcreate-next-app@16.4.4 --yes নিজেই next.config.ts-এ cacheComponents: true আর partialPrefetching: true লিখে দেয় (পর্ব ৫১-এর সত্যিকারের run)

নিজের প্রজেক্ট কোন মডেলে আছে তা জানতে next.config খুলুন, অথবা next build-এর শুরুর লাইন পড়ুন: Cache Components চালু থাকলে build নিজেই - Cache Components enabled লেখে। এই পর্বে দুটি মডেলই সত্যিকারের build দিয়ে দেখানো হবে, কারণ বাস্তবের অনেক প্রজেক্ট এখনো আগের মডেলে চলে। তাই পর্বের যেখানেই “ডিফল্টে” বলা হয়েছে, সাথে লেখা আছে কোন config-এ: অংশ ২, ৩ আর ৪-এর lab-এ next.config নেই, অর্থাৎ cacheComponents বন্ধ; অংশ ৫-এর lab-এ চালু; অংশ ৭-এর lab-এ শুধু output: 'export'.

চিহ্নbuild-এর নিজের ব্যাখ্যাআমাদের কোন build-এ দেখা গেছে
○Static: prerendered as static contentতিনটিতেই
●SSG: prerendered as static HTML (uses generateStaticParams)cacheComponents বন্ধ, আর export
ƒDynamic: server-rendered on demandশুধু cacheComponents বন্ধ অবস্থায়
◐Partial Prerender: static HTML with dynamic server-streamed contentশুধু cacheComponents চালু অবস্থায়; সেখানে ● বা ƒ একটিও আসেনি

Next.js-এ একটি পাতার HTML কোথা থেকে আসে

Build time, the ISR cache, request time and the static export path

Next.js-এর rendering পথ: build, ISR cache, request time আর static export next build · ব্যবহারকারী আসার আগে ○ Static · ● SSG generateStaticParams-এর পাতা ◐ পাতার static shell-ও এখানেই ISR cache · ডিস্কে বা CDN-এ HIT · STALE · MISS সময় পেরোলে বা revalidatePath ডাকলে পেছনে নতুন render, আবার cache-এ অনুরোধের সময় · Node সার্ভার ƒ Dynamic · ◐-এর গর্ত cookies(), headers(), searchParams Suspense-এর ভেতর থেকে stream output: 'export' → শুধু out/ ফোল্ডার, সার্ভার নেই
ছবি ৫৫.১ — ওপরের স্তরের কাজ একবার, মাঝের স্তরের কাজ মাঝে মাঝে, নিচের স্তরের কাজ প্রতিটি অনুরোধে। খরচও এই ক্রমে বাড়ে।

এবার একটি পাতা build-এর সময়ে, আর আরেকটি অনুরোধের সময়ে কী করে, তা পাশাপাশি দেখুন। সংখ্যাগুলো আমাদের সত্যিকারের run থেকে, যার পুরো কোড পরের দুই অংশে আছে।

  1. next buildআগে থেকে
  2. next startসার্ভার চালু
  3. /products/1তৈরি ফাইল
  4. /searchএখনই render
দুটি পাতার অবস্থা
/products/1
/search?q=rice
TTFB, median
  1. Build. /products/1-এর component চলল, ডেটাবেস থেকে দাম আনল, HTML লিখে রাখল। /search-এর component একবারও চলেনি, কারণ সে searchParams পড়ে, যা build-এর সময় অজানা।
  2. সার্ভার চালু। এখনো কোনো ব্যবহারকারী আসেননি। তৈরি পাতাটি ডিস্কে বসে আছে।
  3. /products/1-এ অনুরোধ। কোনো component চলল না, ডেটাবেসেও যাওয়া হলো না। ফাইলটি পাঠানো হলো, header-এ x-nextjs-cache: HIT. আমাদের মেশিনে প্রথম byte এল গড়ে প্রায় 1.8 ms-এ (২১ বার মাপার median, এক local run).
  4. /search?q=rice-এ অনুরোধ। এবার component চলল, ইচ্ছা করে রাখা 50 ms-এর নকল ডেটাবেস দেরি পেরোল, তারপর HTML. প্রথম byte 66.6 ms-এ। Header-এ private, no-store, কারণ উত্তরটি এই অনুরোধের জন্যই।
  5. আবার /search. আবার পুরো কাজ, কারণ কিছুই জমা রাখা হয়নি। এক পাতার খরচ build-এ একবার, অন্যটির খরচ প্রতিবার। দুটির পার্থক্য প্রায় পুরোটাই ডেটাবেসের ওই 50 ms আর render-এর কাজ।

২ · SSG: generateStaticParams দিয়ে build-এ পণ্যের পাতা

“বাজারঘর”-এর পণ্যের পাতার ঠিকানা /products/[id]. বন্ধনীর ভেতরের id একটি dynamic অংশ, তাই Next.js নিজে থেকে জানে না কোন কোন id-র পাতা বানাতে হবে। এই তালিকা দেয় generateStaticParams: build-এর সময় একবার চলা একটি ফাংশন, যা প্রতিটি পাতার params ফেরত দেয়।

আমাদের ছোট lab-এ তিনটি পণ্য আছে, আর ডেটা আসে একটি JSON ফাইল থেকে, যাতে চলতি সার্ভারেও দাম বদলানো যায়। প্রতিটি ডেটা পড়ায় ইচ্ছা করে 50 ms দেরি রাখা হয়েছে, যেন একটি আসল ডেটাবেসের মতো লাগে। ধরা যাক জনপ্রিয় পণ্য মাত্র দুটি, তাই build-এ শুধু সেই দুটি বানাই:

import { notFound } from 'next/navigation';
import { getProduct, getProducts } from '../../../lib/data';

export const revalidate = 10; // শুধু demo-র জন্য ছোট; আসল দোকানে অনেক বড়

export async function generateStaticParams() {
  const all = await getProducts();
  return all.slice(0, 2).map((p) => ({ id: p.id })); // জনপ্রিয় ২টি build-এ
}

export default async function ProductPage({ params }) {
  const { id } = await params;           // Next.js 15 থেকে params একটি Promise
  const p = await getProduct(id);
  if (!p) notFound();                    // না থাকলে সত্যিকারের 404
  return (
    <main>
      <h1>{p.name}</h1>
      <p>price: {p.price} BDT</p>
      <p>generatedAt: {new Date().toISOString()}</p>
    </main>
  );
}

generatedAt লাইনটি শেখার জন্য রাখা, যাতে HTML দেখে বোঝা যায় পাতাটি ঠিক কখন তৈরি হয়েছিল। Next.js 15 থেকে params একটি Promise, তাই await লাগে; পুরোনো টিউটোরিয়ালে params.id সরাসরি পড়া দেখলে বুঝবেন সেটি আগের সংস্করণের। এবার পুরো অ্যাপের build (সত্যিকারের run; Next.js 16.4.4, React 19.3.1, Node 24, আগের মডেল):

~/bazarghar$ npx next build
▲ Next.js 16.4.4 (Turbopack)
…
✓ Generating static pages using 8 workers (8/8) in 519ms
  Finalizing page optimization ...

Route (app)          Revalidate  Expire
┌ ○ /                        1h      1y
├ ○ /_not-found
├ ƒ /api/revalidate
├ ƒ /cart
├   /products/[id]
│ ├ ● /products/1           10s      1y
│ └ ● /products/2           10s      1y
└ ƒ /search

○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML (uses generateStaticParams)
ƒ  (Dynamic)  server-rendered on demand

এই table-টিই আপনার প্রথম যাচাইয়ের জায়গা। /products/1 আর /products/2-এর পাশে ●: build-এ তৈরি হয়েছে, generateStaticParams থেকে। /products/3 table-এ নেই, কারণ তালিকায় ছিল না। Revalidate কলামে 10s আমাদের revalidate = 10, আর হোমের 1h এসেছে হোমের revalidate = 3600 থেকে। Expire 1y মানে পুরোনো কপি সর্বোচ্চ কত দিন রাখা চলবে। /cart, /search আর route handler পেয়েছে ƒ, অর্থাৎ প্রতি অনুরোধে।

তালিকার বাইরের পণ্য এলে কী হয়

ডিফল্টে dynamicParams সত্য, তাই অজানা id-র পাতা প্রথম অনুরোধে তৈরি হয়ে cache-এ যায়। সত্যিকারের run-এ /products/3-এর প্রথম অনুরোধে header ছিল x-nextjs-cache: MISS, পরেরগুলোতে HIT. Header-টির চারটি মান Next.js-এর নথিতে আছে: HIT (cache থেকে), STALE (cache থেকে, পেছনে নতুন তৈরি চলছে), MISS (cache-এ নেই, এখনই render), আর REVALIDATED (on-demand revalidation-এ নতুন করা)।

মূল ধারণা

সব পাতা build-এ বানানো জরুরি নয়। ২০ হাজার পণ্যের দোকানে প্রতিটি build-এ ২০ হাজার পাতা মানে লম্বা build আর বেশি ফাইল। প্রচলিত কৌশল: জনপ্রিয় কয়েকশো পাতা generateStaticParams-এ, বাকিগুলো প্রথম অনুরোধে। যে পণ্য আসলেই নেই, তার জন্য notFound() ডাকুন, নইলে ভুল URL-ও cache-এ জমবে। তালিকার বাইরের কোনো id-ই না চাইলে export const dynamicParams = false দিন; তখন সেগুলো সরাসরি 404.

৩ · Dynamic rendering: কী জিনিস পাতাকে প্রতি অনুরোধে চালায়

Dynamic rendering মানে পাতাটি প্রতি অনুরোধে সার্ভারে তৈরি হয়, অর্থাৎ SSR. আগের মডেলে এটি সাধারণত আপনার বেছে নেওয়া নয়, বরং কোডের ফল। কোনো component যদি এমন তথ্য চায় যা শুধু অনুরোধের মুহূর্তে জানা যায়, তাহলে পুরো route dynamic হয়ে যায়। Next.js-এর নথি এদের বলে request-time বা runtime API:

যা পড়লেকেন build-এ জানা যায় না“বাজারঘর”-এ উদাহরণ
await cookies()প্রতিটি ব্রাউজারের cookie আলাদাকার্ট, লগইন
await headers()ভাষা, ডিভাইস, অনুরোধকারী আলাদাAccept-Language দেখে ভাষা
searchParamsURL-এর ?q= অংশ অসংখ্য রকমখোঁজ আর filter
await connection()স্পষ্ট করে বলা: “এখান থেকে অনুরোধের সময়”প্রতি অনুরোধে নতুন id
fetch(url, { cache: 'no-store' }) বা revalidate = 0নথি অনুযায়ী এটি route-কে dynamic করেলাইভ স্টক

কার্টের পাতা এমনই। কোড ছোট, কিন্তু একটি লাইন পুরো সিদ্ধান্ত বদলে দেয়:

import { cookies } from 'next/headers';

export default async function Cart() {
  const store = await cookies();   // এই লাইনই route-কে ƒ করে
  const items = Number(store.get('cart_items')?.value ?? 0);
  return <h1>Cart items: {items}</h1>;
}

এবার next start চালিয়ে আসল header দেখি (সত্যিকারের run, curl 8.18):

$ curl -sI http://127.0.0.1:3561/cart
HTTP/1.1 200 OK
Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
X-Powered-By: Next.js
Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
Content-Type: text/html; charset=utf-8
…

দুটি জিনিস লক্ষ করুন। প্রথমত, x-nextjs-cache header নেই, কারণ এই উত্তর কোনো cache থেকে আসেনি। দ্বিতীয়ত, Cache-Control-এ private আর no-store: ব্রাউজার বা পথের কোনো ভাগ করা cache যেন এটি জমা না রাখে। একজনের কার্ট আরেকজনের কাছে যাওয়ার দরজা এখানেই বন্ধ। Vary-এর লম্বা তালিকা Next.js-এর নিজের navigation header; এগুলো আলাদা হলে উত্তরও আলাদা, তাই cache-কে তা জানানো হয়।

একটি সাধারণ ফাঁদ: layout-এ একটি cookies() বসালে, যেমন হেডারে “সালাম, রাফি” দেখাতে, সেই layout-এর নিচের সব পাতা আগের মডেলে dynamic হয়ে যায়। হোম, পণ্য, ব্লগ সবকিছু। Build table-এ হঠাৎ সব ○ বদলে ƒ হলে প্রথমে layout দেখুন। এই সমস্যার পরিষ্কার সমাধানই partial prerendering, যা ৫ নম্বর অংশে আসছে।

৪ · ISR: সময় ধরে আর দরকারমতো নতুন করা

SSG-এর পাতা দ্রুত, কিন্তু দাম বদলালে পুরোনো থেকে যায়। ISR এর মাঝামাঝি পথ: পাতা static থাকে, কিন্তু একটি নির্দিষ্ট সময় পরে প্রথম অনুরোধ এলে পেছনে নতুন করে তৈরি হয়। নীতিটির নাম stale-while-revalidate. Next.js-এর নথির ভাষায়, সময় পেরোনোর পর পরের অনুরোধ “still return the cached (now stale) page”, আর একই সাথে পেছনে নতুন সংস্করণ তৈরি শুরু হয়।

সত্যিকারের run: timestamp ধরে

আমরা /products/1-এ কয়েকবার অনুরোধ করেছি, আর মাঝপথে JSON ফাইলে চালের দাম 450 থেকে 470 করেছি। t হলো এই পরীক্ষা শুরুর পর কত সেকেন্ড; at হলো HTML-এর ভেতরের generatedAt সময় (UTC). পাতাটি এই পরীক্ষার ঠিক আগে 17:35:40-এ তৈরি হয়েছিল:

--- B. ISR timeline ---
  0.0s /products/1    200 HIT         h1=Rice 5kg price=450 at=17:35:40
  1.0s /products/1    200 HIT         h1=Rice 5kg price=450 at=17:35:40
  1.0s    [db] price of 1 set to 470
  3.0s /products/1    200 HIT         h1=Rice 5kg price=450 at=17:35:40
 12.0s /products/1    200 STALE       h1=Rice 5kg price=450 at=17:35:40
 13.0s /products/1    200 HIT         h1=Rice 5kg price=470 at=17:35:53
 13.0s /products/1    200 HIT         h1=Rice 5kg price=470 at=17:35:53
/products/1, Next.js 16.4.4, next start
t
ডেটাবেসে দাম
x-nextjs-cache
ক্রেতা দেখলেন
পাতার generatedAt
পেছনের কাজ
  1. 0.0 s. পাতাটি 17:35:40-এ তৈরি, বয়স 10 s-এর কম। Cache থেকে সরাসরি উত্তর, HIT, দাম 450.
  2. 1.0 s. Admin দাম 470 করলেন। Cache-এর পাতা এ কথা জানে না; কেউ তাকে জানায়নি।
  3. 3.0 s. এখনো HIT, দাম 450. পাতাটির বয়স এখনো 10 s-এর কম, তাই নিয়ম অনুযায়ী এটি “তাজা”, যদিও ডেটাবেসে দাম আলাদা।
  4. 12.0 s. বয়স 10 s পেরিয়েছে। ক্রেতা তবু অপেক্ষা ছাড়াই পুরোনো পাতা পেলেন, STALE, দাম 450. এই অনুরোধটিই পেছনে নতুন render চালু করল।
  5. 13.0 s. নতুন পাতা তৈরি, generatedAt 17:35:53. এবার HIT, দাম 470. দাম বদলের পর মোট 12 s ধরে পুরোনো দাম দেখা গেছে।

এই run থেকে তিনটি সত্য বেরিয়ে আসে, যা মুখস্থ সংজ্ঞায় প্রায়ই হারায়। প্রথমত, revalidate = 10 মানে “প্রতি 10 s-এ নতুন হবে” নয়; মানে “10 s পরের প্রথম অনুরোধে নতুন করার কাজ শুরু হবে”। কেউ না এলে পাতা যেমন আছে তেমনই থাকে। দ্বিতীয়ত, যিনি নতুন render চালু করলেন, তিনি নিজে পুরোনো পাতা পেলেন। তৃতীয়ত, দাম বদলের পর থেকে ক্রেতা নতুন দাম দেখা পর্যন্ত দেরি সর্বোচ্চ প্রায় window-এর সমান, তারপর আরও একটি অনুরোধ।

নিচের teaching model একই যুক্তি একটি নকল ঘড়িতে চালায়, আর SSG, SSR ও ISR-এ প্রতিটি দর্শক কী দেখেন তা পাশাপাশি দেখায়। এটি Next.js নয়, শুধু নিয়মটির সরল রূপ:

// Teaching model, not Next.js itself. Clock is fake and counted in seconds.
const priceAt = (t) => (t >= 20 ? 470 : 450);   // admin changes the price at t = 20 s
const visits = [5, 30, 61, 62, 125, 126];      // when visitors open /products/1

const strategies = {
  SSG() {                                       // built once, at t = 0
    const built = priceAt(0);
    return () => [built, 'built copy'];
  },
  SSR() {                                       // fresh render on every visit
    return (t) => { this.renders++; return [priceAt(t), 'rendered now']; };
  },
  ISR(win = 60, regenSeconds = 1) {             // revalidate = 60
    let page = { price: priceAt(0), at: 0 }, next = null;
    return (t) => {
      if (next && t >= next.ready) { page = next.page; next = null; }
      if (t - page.at < win) return [page.price, 'HIT'];
      if (!next) { this.renders++; next = { page: { price: priceAt(t), at: t }, ready: t + regenSeconds }; }
      return [page.price, 'STALE, regenerating in background'];
    };
  },
};

for (const [name, make] of Object.entries(strategies)) {
  const ctx = { renders: 1 };                   // the build itself is one render
  if (name === 'SSR') ctx.renders = 0;
  const serve = make.call(ctx);
  console.log(name);
  for (const t of visits) {
    const [price, note] = serve(t);
    const wrong = price !== priceAt(t) ? '  <- old price' : '';
    console.log(`  t=${String(t).padStart(3)}s  shows ${price}  ${note}${wrong}`);
  }
  console.log(`  renders: ${ctx.renders}`);
}

আউটপুট:

SSG
  t=  5s  shows 450  built copy
  t= 30s  shows 450  built copy  <- old price
  t= 61s  shows 450  built copy  <- old price
  t= 62s  shows 450  built copy  <- old price
  t=125s  shows 450  built copy  <- old price
  t=126s  shows 450  built copy  <- old price
  renders: 1
SSR
  t=  5s  shows 450  rendered now
  t= 30s  shows 470  rendered now
  t= 61s  shows 470  rendered now
  t= 62s  shows 470  rendered now
  t=125s  shows 470  rendered now
  t=126s  shows 470  rendered now
  renders: 6
ISR
  t=  5s  shows 450  HIT
  t= 30s  shows 450  HIT  <- old price
  t= 61s  shows 450  STALE, regenerating in background  <- old price
  t= 62s  shows 470  HIT
  t=125s  shows 470  STALE, regenerating in background
  t=126s  shows 470  HIT
  renders: 3

তিনটি কৌশলের দাম আর সঠিকতা একসাথে দেখা যাচ্ছে। SSG একবার render করে, কিন্তু নতুন build না হলে চিরকাল ভুল দাম দেখায়। SSR সবসময় ঠিক, কিন্তু ছয় দর্শকে ছয়টি render. ISR তিনটি render-এ দুটি ভুল দামের দর্শক মেনে নেয়। খেয়াল করুন t=62 থেকে t=125: কোনো দর্শক নেই, তাই কোনো কাজও নেই। visits-এ আরও সময় যোগ করে, বা win 10 করে আবার চালিয়ে দেখুন।

On-demand revalidation: সময়ের অপেক্ষা না করে

দাম বদলালে admin প্যানেল জানে ঠিক কোন পাতা পুরোনো হলো। তাহলে সময়ের অপেক্ষা কেন? On-demand revalidation-এ কোড থেকে নির্দিষ্ট পাতাকে বাতিল ঘোষণা করা হয়। নিচে একটি route handler, যা admin প্যানেল দাম বদলের পর ডাকবে। Route handler-এর বিস্তারিত পর্ব ৫৭-এ, আর Server Action থেকে একই কাজ পর্ব ৫৪-এ:

import { revalidatePath } from 'next/cache';

// Admin প্যানেল দাম বদলের পর ডাকে। Secret এখানে নকল demo মান।
export async function POST(request) {
  if (request.headers.get('x-revalidate-secret') !== process.env.REVALIDATE_SECRET) {
    return Response.json({ ok: false }, { status: 401 });
  }
  const { id } = await request.json();
  if (!/^\d+$/.test(String(id))) return Response.json({ ok: false }, { status: 400 });
  revalidatePath(`/products/${id}`);
  return Response.json({ ok: true, revalidated: `/products/${id}` });
}

Secret না মেলালে যে কেউ বারবার এই URL ডেকে আপনার সার্ভারকে অকারণ render-এ ব্যস্ত রাখতে পারত। Id-টিও যাচাই করা হয়েছে, যাতে ইচ্ছামতো path পাঠানো না যায়। Secret-টি আসে environment variable থেকে; run-এ মান ছিল example-secret. বাস্তবে আরও নিরাপদ হয় সময়-নিরপেক্ষ তুলনা আর rate limit যোগ করলে, যার তত্ত্ব আছে Cybersecurity-র API security পর্বে। একই সার্ভারে সত্যিকারের run:

--- C. on-demand ---
 13.0s    [db] price of 1 set to 480
 13.1s /products/1    200 HIT         h1=Rice 5kg price=470 at=17:35:53
 13.1s POST wrong secret -> 401 {"ok":false}
 13.1s POST right secret -> 200 {"ok":true,"revalidated":"/products/1"}
 13.1s /products/1    200 MISS        h1=Rice 5kg price=480 at=17:35:55
 13.1s /products/1    200 HIT         h1=Rice 5kg price=480 at=17:35:55

দাম 480 হওয়ার পরও প্রথম অনুরোধে 470, কারণ window এখনো চলছে। ভুল secret-এ 401, ঠিক secret-এ 200. তারপরের অনুরোধে MISS: পুরোনো পাতা বাতিল, তাই এই ক্রেতা অপেক্ষা করে নতুন render পেলেন, দাম 480. অর্থাৎ revalidatePath নিজে পাতা বানায় না, শুধু বাতিল করে; নতুন পাতা হয় পরের অনুরোধে। নথিও ঠিক এটাই বলে। কয়েকটি পাতা একসাথে বাতিল করতে tag ব্যবহার হয়; Next.js 16-এ নথির উদাহরণ revalidateTag('posts', 'max'), অর্থাৎ দ্বিতীয় একটি argument লাগে। পুরোনো টিউটোরিয়ালে এক argument দেখলে নিজের সংস্করণের নথি মিলিয়ে নিন।

একাধিক সার্ভারে সাবধান: নথি অনুযায়ী ডিফল্ট file-system cache প্রতিটি instance-এর নিজের। দুটি সার্ভার চালালে revalidatePath শুধু সেই instance-এর cache বাতিল করে, যে অনুরোধটি পেল। অন্যটি পুরোনো পাতা দিতেই থাকবে। সমাধান হলো একটি ভাগ করা cache handler, যার ধারণা পর্ব ৪৪-এর Redis cache-এর সাথে মেলে। আরেকটি সীমা: ISR শুধু Node.js runtime-এ চলে, আর static export-এ একেবারেই নেই।

৫ · Partial prerendering: static shell আর dynamic গর্ত

আগের মডেলের সবচেয়ে বড় সীমা: একটি route হয় পুরো static, নয়তো পুরো dynamic. পণ্যের পাতায় নাম, ছবি আর দাম সবার জন্য এক, কিন্তু “স্টকে কতটি আছে” প্রতি মিনিটে বদলায়। শুধু স্টকের জন্য পুরো পাতা dynamic করলে সব ক্রেতাকে প্রতিবার পুরো render-এর দাম দিতে হয়।

Partial prerendering, সংক্ষেপে PPR, এই ভাগ ভাঙে। Build-এর সময় পাতার যতটা পারা যায় তৈরি হয়ে একটি static shell হয়। যে অংশ অনুরোধ ছাড়া বানানো যায় না, তা <Suspense>-এর ভেতরে থাকে; shell-এ যায় শুধু তার fallback, আর আসল অংশ অনুরোধের সময় একই উত্তরের ভেতরে stream হয়ে আসে।

নাম নিয়ে একটু ইতিহাস

PPR প্রথমে পরীক্ষামূলক flag হিসেবে আসে (Next.js 14, ২০২৩ সালের দিকে)। Next.js 15-এ এটি ছিল experimental.ppr. Next.js 16-এ নথি অনুযায়ী cacheComponents স্থিতিশীল হয়, আর PPR তার ডিফল্ট আচরণ হয়ে যায়; পুরোনো experimental.ppr flag আর route-এর experimental_ppr সরিয়ে দেওয়া হয়েছে। অর্থাৎ আজ “PPR চালু করা” মানে cacheComponents: true. লেখার সময়ের নথি এর সাথে partialPrefetching: true-ও স্পষ্ট করে লিখতে বলে; না লিখলে সতর্কবার্তা আসে।

/** @type {import('next').NextConfig} */
const nextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
};

export default nextConfig;

পণ্যের পাতা এবার দুই ভাগে। পণ্যের তথ্য আসে 'use cache' দেওয়া একটি ফাংশন থেকে, যার আয়ু cacheLife('hours'). স্টক আসে একটি ধীর API থেকে, যেখানে ইচ্ছা করে 800 ms দেরি রাখা হয়েছে, আর connection() স্পষ্ট বলে দেয় “এখান থেকে অনুরোধের সময়”:

import { Suspense } from 'react';
import { connection } from 'next/server';
import { getProduct, getStock } from '../../../lib/data'; // getProduct-এ 'use cache'

export async function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }];
}

async function ProductInfo({ params }) {
  const { id } = await params;
  const p = await getProduct(id);          // cache-করা: shell-এর অংশ
  return <h1>{p.name}: {p.price} BDT</h1>;
}

async function Stock({ params }) {
  const { id } = await params;
  await connection();                      // এখান থেকে প্রতি অনুরোধে
  const n = await getStock(id);            // ধীর API: 800 ms
  return <p>In stock: {n} (checked {new Date().toISOString()})</p>;
}

export default function ProductPage({ params }) {
  return (
    <main>
      <Suspense fallback={<h1>Loading product...</h1>}><ProductInfo params={params} /></Suspense>
      <Suspense fallback={<p>Checking stock...</p>}><Stock params={params} /></Suspense>
    </main>
  );
}

লক্ষ করুন, পাতার মূল ফাংশন নিজে params-এর await করে না, Promise-টি নিচে পাঠায়। নথি একে বলে static shell বড় করা: async কাজ গাছের যত গভীরে, তত বেশি অংশ আগে থেকে বানানো যায়। Build output (সত্যিকারের run, একই সংস্করণ):

~/bazarghar-ppr$ npx next build
▲ Next.js 16.4.4 (Turbopack)
✓ Running next.config.mjs took 18ms
- Cache Components enabled
- Partial Prefetching enabled
…
Route (app)
┌ ◐ /
├ ○ /_not-found
├ ◐ /cart
└   /products/[id]
  ├ ◐ /products/[id]
  ├ ◐ /products/1
  └ ◐ /products/2

○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

এখানে কোনো ƒ নেই। কার্টও এখন ◐: তার “Your cart” শিরোনাম আর “Loading cart...” fallback shell-এ, শুধু cookie-নির্ভর সংখ্যাটি stream হয়। /products/[id] নিজেও একটি ◐ লাইন পেয়েছে: এটি App Shell, যা তালিকার বাইরের id-র প্রথম অনুরোধে সঙ্গে সঙ্গে যায়। আর আগের মডেলের Revalidate কলাম নেই; আয়ু এখন cacheLife-এর ভেতরে।

তারে কী আসে: chunk ধরে ধরে

একটি ছোট Node script দিয়ে /products/1-এর উত্তরের প্রতিটি টুকরো কখন এল তা মেপেছি। এটি দ্বিতীয় অনুরোধ, সার্ভার গরম হওয়ার পরে (এক local run):

GET /products/1  status 200  headers at     5 ms {"x-nextjs-stale-time":"300","x-nextjs-prerender":"1",
  "x-nextjs-postponed":"1","cache-control":"private, no-cache, no-store, max-age=0, must-revalidate",
  "transfer-encoding":"chunked"}
  chunk 1 at     6 ms    1038 bytes  Rice 5kg | Checking stock...
  chunk 2 at    11 ms    5050 bytes  …
  chunk 3 at    24 ms    4727 bytes  …
  chunk 4 at   824 ms     150 bytes  In stock
  …
  end at   824 ms

প্রথম টুকরোর HTML, script বাদ দিয়ে (সত্যিকারের run, একটু ছেঁটে):

<main><!--$--><h1>Rice 5kg<!-- -->: <!-- -->450<!-- --> BDT</h1><!--/$-->
<!--$?--><template id="B:0"></template><p>Checking stock...</p><!--/$--></main>
…
===== 800 ms পরের টুকরো =====
<div hidden id="S:0"><p>In stock: <!-- -->12<!-- --> (checked <!-- -->2026-11-02T17:37:08.104Z<!-- -->)</p></div><script>…</script>
  1. অনুরোধ0 ms
  2. Static shell6 ms
  3. RSC payload24 ms
  4. স্টকের HTML824 ms
ব্যবহারকারীর পর্দায়
শিরোনাম
স্টকের জায়গা
সংযোগ
  1. 0 ms. ক্রেতা পণ্যের পাতা খুললেন। সার্ভারে shell আগে থেকেই তৈরি, আর একই সাথে স্টকের কাজ শুরু হলো।
  2. 6 ms. প্রথম টুকরো, 1038 byte: পণ্যের নাম আর দাম আছে, স্টকের জায়গায় Checking stock... আর একটি ফাঁকা <template id="B:0">, যা পরে ভরার চিহ্ন। ক্রেতা আসল বিষয়বস্তু প্রায় সঙ্গে সঙ্গে দেখছেন।
  3. 24 ms. পরের দুই টুকরো মূলত script: RSC payload, যা দিয়ে React পাতাটি hydrate করবে আর পরের navigation চালাবে। পর্দায় নতুন কিছু নেই।
  4. 824 ms. 800 ms-এর ধীর API শেষ। একটি লুকানো <div hidden id="S:0">-এ স্টকের HTML এল, আর ছোট একটি script সেটিকে B:0-এর জায়গায় বসিয়ে fallback সরাল। কোনো আলাদা অনুরোধ লাগেনি, একই উত্তরের ভেতরে এসেছে।
  5. শেষ। </body></html> এসে সংযোগ বন্ধ। curl-এ ২১ বারের median: প্রথম byte 2.4 ms, পুরো উত্তর 815.5 ms. TTFB ধীর অংশের ওপর আর নির্ভর করছে না।

একই ধারণা ব্রাউজারে নিজে চালিয়ে দেখুন। নিচের ব্লকে সত্যিকারের React 19 চলে। এটি Next.js নয়, সার্ভারও নেই; একটি Promise 800 ms পরে মান দেয়, আর use সেই Promise-এর অপেক্ষা করে। কিন্তু দেখার অভিজ্ঞতা PPR-এর মতোই: shell আগে, গর্ত পরে।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const t0 = performance.now();
const at = () => Math.round((performance.now() - t0) / 100) * 100 + ' ms';
const stock = new Promise((ok) => setTimeout(() => ok(12), 800)); // ধীর API

function Stock() {
  const n = use(stock);                 // তৈরি না হলে Suspense-এর fallback
  console.log('hole filled at about', at());
  return <p>স্টকে আছে: {n}টি</p>;
}

function ProductPage() {
  console.log('shell drawn at about', at());
  return (
    <main>
      <h1>Rice 5kg: 450 BDT</h1>
      <Suspense fallback={<p>স্টক দেখা হচ্ছে...</p>}>
        <Stock />
      </Suspense>
    </main>
  );
}
createRoot(document.getElementById('root')).render(<ProductPage />);
</script>

Edge-এ “প্রিভিউ দেখুন” চেপে ৩ সেকেন্ড পরে console-এ যা এল (পর্দায় প্রথমে “স্টক দেখা হচ্ছে...”, পরে “স্টকে আছে: 12টি”):

shell drawn at about 0 ms
hole filled at about 800 ms

শিরোনাম সঙ্গে সঙ্গে, স্টক প্রায় 800 ms পরে। সময়গুলো ১০০ ms-এ গোল করা, কারণ প্রতিটি ব্রাউজারে সামান্য আলাদা হবে। 800 বদলে 3000 করে চালান; শিরোনামের সময় একটুও বদলাবে না। Next.js-এ পার্থক্য হলো shell আসে আগে থেকে তৈরি HTML হিসেবে সার্ভার থেকে, আর গর্ত ভরে সার্ভারের stream দিয়ে, ব্রাউজারের Promise দিয়ে নয়।

তালিকার বাইরের পণ্য আর App Shell

/products/3 generateStaticParams-এ ছিল না। Cache Components মডেলে প্রথম অনুরোধে সত্যিকারের run-এ প্রথম টুকরোয় এল Loading product..., মানে App Shell; পণ্যের নাম এল 23 ms-এ, একই উত্তরে stream হয়ে। দেড় সেকেন্ড পরের অনুরোধে প্রথম টুকরোতেই Soybean oil: পেছনে পাতাটি “উন্নত” হয়ে cache-এ গেছে। নথি এটিকেই Cache Components-এর ISR বলে, আর জানায় এই App Shell আচরণ Next.js 16.3 থেকে।

Header দেখে চমকাবেন না: আমাদের self-hosted next start-এ ◐ পাতার পুরো উত্তরের Cache-Control ছিল private, no-store, আর x-nextjs-cache ছিল না। কারণটি যুক্তিসঙ্গত: উত্তরটির শেষ অংশ এই অনুরোধের, তাই পুরো উত্তর ভাগ করা cache-এ রাখা নিরাপদ নয়। Shell-টি Next.js সার্ভারের ভেতরে জমা থাকে, তাই TTFB কম। একটি সাধারণ CDN কিন্তু এই উত্তর জমা রাখবে না। নথি বলে static shell CDN থেকে দেওয়া যায়; তবে তার জন্য hosting platform-কে shell আর stream আলাদা করে সামলাতে জানতে হয়, যা প্রদানকারী ভেদে আলাদা। আপনার hosting-এ কী হয়, তা header দেখে নিজে যাচাই করুন।

৬ · Edge বনাম Node runtime: একটু সাবধানে

“Edge” শব্দটি দুই অর্থে ব্যবহৃত হয়, আর দুটি গুলিয়ে ফেলা খুব সাধারণ। প্রথম অর্থ: CDN-এর edge server, যা ব্যবহারকারীর কাছে ফাইল জমা রাখে। Static পাতা আর static shell এখান থেকে দিলে দূরত্ব কমে। দ্বিতীয় অর্থ: Next.js-এর Edge runtime, একটি হালকা JavaScript পরিবেশ, যেখানে শুধু Web API চলে আর Node.js-এর অনেক module (যেমন fs) নেই।

কয়েক বছর ধরে কথা ছিল: route-কে export const runtime = 'edge' দিলে কোড ব্যবহারকারীর কাছে চলবে, তাই দ্রুত হবে। লেখার সময়ের (16.4) Next.js নথি অবস্থান বদলেছে। নথিতে 'edge' মানটি “deprecated” বলে চিহ্নিত, আর পরামর্শ হলো runtime export মুছে ফেলা, কারণ Node.js-ই ডিফল্ট। Cache Components চালু থাকলে runtime export, এমনকি 'nodejs'-ও, রাখা যাবে না। ISR-ও নথি অনুযায়ী শুধু Node.js runtime-এ চলে।

বিষয়Node.js runtimeEdge runtime
লেখার সময়ে Next.js 16-এডিফল্ট, সুপারিশকৃতdeprecated
Node module (fs, অনেক ডেটাবেস driver)পাওয়া যায়অনেকগুলোই নেই
ISR আর Cache Componentsসমর্থিতসমর্থিত নয়
কোথায় চলেআপনার সার্ভার বা platform-এর অঞ্চলplatform ভেদে ব্যবহারকারীর কাছাকাছি

কেন এই উল্টো যাত্রা, তা নিয়ে community-তে নানা মত আছে, আর ভাবনার প্রশ্ন ৪-এ বিস্তারিত আছে। মূল যুক্তিটি সহজ: পাতার কাজ যদি ডেটাবেস থেকে ডেটা আনা হয়, আর ডেটাবেস থাকে একটি অঞ্চলে, তাহলে কোড ব্যবহারকারীর কাছে চালালে প্রতিটি query-কে সেই দূরত্ব পার হতে হয়। কয়েকটি query হলে মোট সময় বরং বাড়ে। Proxy (আগের middleware) কোথায় চলে, তা পর্ব ৫৭-এর বিষয়।

মূল ধারণা

দ্রুততার জন্য “কোড edge-এ চালানো”-র চেয়ে সাধারণত বেশি কাজে দেয় “তৈরি HTML edge-এ রাখা”। অর্থাৎ যতটা পারা যায় static বা static shell, আর বাকি কাজ ডেটাবেসের কাছের Node সার্ভারে।

৭ · Static export: shared hosting-এ সস্তায়

বাংলাদেশে অনেক ছোট ব্যবসা আর ফ্রিল্যান্সার cPanel-ওয়ালা shared hosting ব্যবহার করেন। সেখানে সাধারণত Apache আর PHP চলে, আর লম্বা সময় ধরে Node সার্ভার চালানোর সুবিধা প্রায়ই থাকে না বা সীমিত থাকে (প্রদানকারী ভেদে আলাদা; পর্ব ৮২-তে বিস্তারিত)। এমন জায়গায় Next.js চালানোর পথ হলো static export: build-এর সময় সব পাতা সাধারণ HTML, CSS আর JS ফাইল হয়ে একটি out ফোল্ডারে যায়, যা যেকোনো web সার্ভার দিতে পারে।

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  trailingSlash: true, // /products/1/index.html: Apache-এর জন্য সুবিধাজনক
};

export default nextConfig;

এই প্রজেক্টে পণ্যের পাতায় dynamicParams = false, কারণ export-এ তালিকার বাইরের পাতা পরে বানানোর কোনো সার্ভার নেই। কার্ট এখানে একটি Client Component, যা ব্রাউজারের localStorage থেকে সংখ্যা পড়ে। সত্যিকারের build:

~/bazarghar-export$ npx next build
▲ Next.js 16.4.4 (Turbopack)
…
Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /cart
└   /products/[id]
  ├ ● /products/1
  ├ ● /products/2
  └ ● /products/3

○  (Static)  prerendered as static content
●  (SSG)     prerendered as static HTML (uses generateStaticParams)

~/bazarghar-export$ find out -type f | grep -v _next | sort
out/404.html
out/404/index.html
out/_not-found/index.html
out/_not-found/index.txt
out/cart/index.html
out/cart/index.txt
out/index.html
out/index.txt
out/products/1/index.html
out/products/1/index.txt
…
~/bazarghar-export$ du -sh out; find out -type f | wc -l
766K	out
44

পুরো দোকান এখন ৪৪টি ফাইল, মোট 766 KB. প্রতিটি পাতার পাশে একটি .txt: এটি সেই পাতার RSC payload, যা দিয়ে ব্রাউজারে পাতা বদলানো (client navigation) চলে। _next ফোল্ডারে JavaScript আর CSS, যাদের নামে hash থাকে। এই ফোল্ডারের ভেতরের সবকিছু cPanel-এর File Manager বা FTP দিয়ে public_html-এ তুলে দিলেই সাইট চালু, কোনো Node লাগে না।

সীমা: সত্যিকারের build কী বলে

নথিতে যা চলে না তার একটি তালিকা আছে: Server Action, ISR, cookie, rewrite, redirect, header config, proxy, ডিফল্ট image optimization, generateStaticParams ছাড়া dynamic route, আর আরও কিছু। কথাগুলো আমরা নিজে ভেঙে দেখেছি। প্রথমে একটি newsletter form-এ Server Action যোগ করলাম:

~/bazarghar-export$ npx next build
…
  Collecting page data using 7 workers ...

> Server Actions are not supported with static export.
Read more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features

তারপর একটি অ্যাকাউন্ট পাতায় cookies():

Error occurred prerendering page "/account". Read more: https://nextjs.org/docs/messages/prerender-error
Error: Route /account with `dynamic = "error"` couldn't be rendered statically because it used `cookies()`. …
Export encountered an error on /account/page: /account, exiting the build.

দুটিতেই build থেমে গেছে, যা ভালো: ভুল ধরা পড়ল আপনার মেশিনে, ক্রেতার কাছে নয়। কিন্তু তৃতীয় পরীক্ষাটি বিপজ্জনক। পণ্যের পাতায় export const revalidate = 60 যোগ করে build চালালাম: build সফল, কোনো সতর্কবার্তা নেই, route table-এ Revalidate কলামও নেই। অর্থাৎ ISR চুপচাপ উপেক্ষা হলো। পরের build না হওয়া পর্যন্ত পাতাগুলো চিরকাল একই থাকবে। যিনি ভাবছেন “60 সেকেন্ডে দাম ঠিক হয়ে যাবে”, তিনি ভুল ভাবছেন।

cPanel-এ তোলার পর

trailingSlash: true দিলে প্রতিটি পাতা নিজের ফোল্ডারের index.html, যা Apache কোনো বাড়তি নিয়ম ছাড়াই দেয়। বাকি দুটি কাজ .htaccess-এ করা যায়: নিজের 404 পাতা দেখানো, আর hash-ওয়ালা ফাইলকে লম্বা সময় cache করতে বলা। নিচেরটি উদাহরণ, আমরা চালাইনি; আপনার hosting-এ mod_headers চালু আছে কি না দেখে নিন:

ErrorDocument 404 /404.html

<IfModule mod_headers.c>
  # HTML: প্রতিবার যাচাই, যাতে নতুন upload সঙ্গে সঙ্গে দেখা যায়
  <FilesMatch "\.html$">
    Header set Cache-Control "no-cache"
  </FilesMatch>
  # _next/static-এর ফাইলের নামে hash আছে, তাই এক বছর নিরাপদ
  <If "%{REQUEST_URI} =~ m#^/_next/static/#">
    Header set Cache-Control "public, max-age=31536000, immutable"
  </If>
</IfModule>
দরকারStatic export-এবিকল্প পথ
পণ্যের তালিকা, ব্লগচমৎকার—
দাম বদলানোনতুন build আর uploadCI দিয়ে স্বয়ংক্রিয় build (পর্ব ৮১)
কার্টব্রাউজারে, localStorageআলাদা API সার্ভারে
লগইন, অর্ডার, পেমেন্টনেইআলাদা backend, যেমন Express বা PHP; সব যাচাই সেখানে
Form জমাServer Action নেইআলাদা API-তে fetch

৮ · মাপুন: TTFB আর CDN-এর header

কৌশলের তুলনা অনুমানে নয়, মেপে করুন। আমরা প্রতিটি পাতায় curl দিয়ে ২১ বার অনুরোধ পাঠিয়ে time_starttransfer, মানে TTFB, মেপেছি আর median নিয়েছি। কমান্ডটি এমন:

$ curl -s -o /dev/null -w "%{time_starttransfer} %{time_total}\n" http://127.0.0.1:3561/products/1
পাতাকৌশলTTFB medianপুরো উত্তর median
/○ static, আগের মডেল1.9 ms—
/products/1● ISR, আগের মডেল1.8 ms—
/products/3প্রথম অনুরোধে তৈরি, তারপর ISR1.9 ms—
/cartƒ dynamic, 50 ms নকল ডেটাবেস66.3 ms—
/search?q=riceƒ dynamic, 50 ms নকল ডেটাবেস66.6 ms—
/products/1◐ PPR, 800 ms ধীর স্টক2.4 ms815.5 ms
/cart◐ PPR, cookie-র গর্ত2.0 ms21.5 ms

এগুলো একটি মেশিনে এক local run-এর সংখ্যা, সাধারণ benchmark নয়। ক্লায়েন্ট আর সার্ভার একই কম্পিউটারে, তাই নেটওয়ার্কের দূরত্ব প্রায় শূন্য। আসল ব্যবহারকারীর TTFB-তে যোগ হবে DNS, TCP, TLS আর দূরত্ব, যা প্রায়ই এই সংখ্যার কয়েক গুণ। তুলনাটি তবু শিক্ষণীয়: static আর PPR পাতার প্রথম byte ডেটাবেসের অপেক্ষা করে না, dynamic পাতারটি করে। আর PPR-এর ধীর অংশ দেরি করায় পুরো উত্তর (815.5 ms), প্রথম byte নয়। Dynamic পাতার 66 ms-এর 50 ms আমাদের ইচ্ছা করে রাখা দেরি; আপনার আসল ডেটাবেস দ্রুত বা ধীর হতে পারে।

Header পড়া: CDN কী বুঝবে

$ curl -sI http://127.0.0.1:3561/products/2
HTTP/1.1 200 OK
Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
x-nextjs-cache: STALE
x-nextjs-prerender: 1
x-nextjs-prerender: 1
x-nextjs-stale-time: 300
X-Powered-By: Next.js
Cache-Control: s-maxage=10, stale-while-revalidate=31535990
ETag: "ya3s6rk6q14h8"
Content-Type: text/html; charset=utf-8
Content-Length: 5804
…

এই একটি উত্তরে কয়েকটি শিক্ষা। x-nextjs-cache: STALE: সার্ভার চালুর অনেক আগে build হয়েছিল, তাই প্রথম অনুরোধেই পুরোনো কপি গেল আর পেছনে নতুন render শুরু হলো। Cache-Control-এ s-maxage=10: CDN-এর মতো ভাগ করা cache 10 s পর্যন্ত এটিকে তাজা ধরবে; সংখ্যাটি আমাদের revalidate-এর সাথে মেলে। stale-while-revalidate=31535990-এর হিসাব দেখুন: 31536000 − 10 = 31535990, অর্থাৎ এক বছর (৩৬৫ দিন) থেকে window বাদ। এটিই route table-এর Expire 1y. ব্রাউজারের জন্য আলাদা max-age নেই, তাই ব্রাউজার নিজে জমা রাখে না। হোমের header-এ ছিল s-maxage=3600, stale-while-revalidate=31532400, একই নিয়মে।

অর্থাৎ ISR পাতার header আসলে CDN-কেও একই stale-while-revalidate নিয়ম মানতে বলে। CDN সামনে থাকলে বেশিরভাগ অনুরোধ Next.js পর্যন্ত আসেই না। তবে কোনো CDN stale-while-revalidate মানে কি না, আর ডিফল্টে HTML cache করে কি না, তা প্রদানকারী আর সেটিং ভেদে আলাদা; তাই CDN-এর নিজের header (যেমন age বা cache-status ধরনের header) দেখে যাচাই করুন। এর তত্ত্ব Computer Networks-এর caching পর্বে আর System Design-এর CDN পর্বে।

খরচের হিসাব: build বনাম অনুরোধ

কৌশলের খরচ মাপার একটি সরল উপায়: দিনে মোট কতবার পাতা render হয়। নিচের মডেলের সব সংখ্যা ধরে নেওয়া, কোনো আসল দোকানের নয়:

// সব সংখ্যা ধরে নেওয়া (assumed), মাপা নয়। বদলে দেখুন।
const products = 20000;          // পণ্যের পাতা
const viewsPerDay = 500000;      // দিনে মোট পণ্য-পাতা দেখা
const priceChangesPerDay = 300;  // দিনে কতবার কোনো একটি পণ্যের দাম বদলায়
const msPerRender = 40;          // একটি পাতা render করতে CPU সময়

// ISR-এ একটি পাতা দিনে সর্বোচ্চ (86400 / window) বার নতুন হতে পারে,
// আর কখনোই দেখার সংখ্যার বেশি নয়, কারণ নতুন হওয়া শুরু হয় অনুরোধ থেকেই।
const isrMax = (windowSec) => Math.min(viewsPerDay, products * Math.floor(86400 / windowSec));

const plans = [
  ['SSR, প্রতিটি দেখায় render', viewsPerDay],
  ['SSG, প্রতিটি দাম বদলে পুরো build', priceChangesPerDay * products],
  ['ISR, revalidate 60 s (সর্বোচ্চ)', isrMax(60)],
  ['ISR, revalidate 1 h (সর্বোচ্চ)', isrMax(3600)],
  ['ISR, 1 দিন + on-demand', isrMax(86400) + priceChangesPerDay],
];

console.log(`গড়ে প্রতিটি পণ্য দিনে ${viewsPerDay / products} বার দেখা হয়`);
for (const [name, renders] of plans) {
  const cpuHours = (renders * msPerRender) / 3600000;
  console.log(name.padEnd(34), String(renders).padStart(8), 'render',
    cpuHours.toFixed(2).padStart(7), 'CPU-ঘণ্টা');
}

আউটপুট:

গড়ে প্রতিটি পণ্য দিনে 25 বার দেখা হয়
SSR, প্রতিটি দেখায় render           500000 render    5.56 CPU-ঘণ্টা
SSG, প্রতিটি দাম বদলে পুরো build    6000000 render   66.67 CPU-ঘণ্টা
ISR, revalidate 60 s (সর্বোচ্চ)      500000 render    5.56 CPU-ঘণ্টা
ISR, revalidate 1 h (সর্বোচ্চ)       480000 render    5.33 CPU-ঘণ্টা
ISR, 1 দিন + on-demand                20300 render    0.23 CPU-ঘণ্টা

ফলটি অনেককে চমকে দেয়। প্রথমত, প্রতিটি দাম বদলে পুরো build সবচেয়ে দামি: 300 × 20000 = 60 লাখ render. দ্বিতীয়ত, গড়ে একটি পণ্য দিনে মাত্র ২৫ বার দেখা হলে 60 s বা 1 h-এর ISR প্রায় SSR-এর সমান খরচের হতে পারে, কারণ প্রায় প্রতিটি দেখাই window পেরিয়ে আসে। ISR-এর লাভ বড় হয় জনপ্রিয় পাতায়, যেখানে একটি window-এর ভেতরে অনেক দর্শক আসেন। তৃতীয়ত, সবচেয়ে সস্তা পথ হলো লম্বা window আর দাম বদলালে শুধু সেই পাতার on-demand revalidation: 20000 + 300 = 20300. 60 s আর 1 h-এর সারি ঊর্ধ্বসীমা; আসল সংখ্যা নির্ভর করে দর্শক দিনে কীভাবে ছড়ানো তার ওপর। viewsPerDay ৫০ লাখ করে আবার চালান, দেখুন কোন সারি বদলায়।

৯ · “বাজারঘর”: পাতা ধরে সিদ্ধান্ত

এবার পুরো দোকান। প্রতিটি পাতার জন্য চারটি প্রশ্ন করুন: (১) এই উত্তর কি সবার জন্য এক, নাকি মানুষ ধরে আলাদা? (২) কত ঘন ঘন বদলায়, আর পুরোনো দেখালে ক্ষতি কত? (৩) কোনো অংশ কি একদম এই মুহূর্তের হতে হবে? (৪) আপনার hosting-এ কি Node সার্ভার চলে? নিচের সিদ্ধান্ত-সহায়ক এই যুক্তি কোড করে। বৈশিষ্ট্য বদলে দেখুন।

// একটি পাতার চাহিদা থেকে কৌশল: শেখার ছক, চূড়ান্ত নিয়ম নয়
function choose(p, host) {
  if (host === 'static') {                       // output: 'export', সার্ভার নেই
    if (p.sharedPart || p.liveBit) return 'SSG + বদলানো অংশ ব্রাউজারে, আলাদা API থেকে';
    if (p.personal) return 'CSR: ব্রাউজারে আঁকা, ডেটা আলাদা API থেকে';
    return p.changesPerDay ? 'SSG: বদলালে আবার build ও upload' : 'SSG';
  }
  if (p.personal && !p.sharedPart) return 'Dynamic: প্রতি অনুরোধে, Cache-Control private';
  if (p.sharedPart && p.personal) return 'PPR: সবার shell + ব্যক্তিগত গর্ত';
  if (p.liveBit) return 'PPR: cache-করা পাতা + live গর্ত';
  if (!p.changesPerDay) return 'SSG';
  return 'ISR: লম্বা window + on-demand revalidation';
}

const pages = [
  { name: 'হোম',         sharedPart: true, personal: true, changesPerDay: 6 },
  { name: 'পণ্যের পাতা', liveBit: true, changesPerDay: 12 },
  { name: 'আমাদের কথা',  changesPerDay: 0 },
  { name: 'ব্লগ তালিকা',  changesPerDay: 2 },
  { name: 'কার্ট',        personal: true },
  { name: 'অ্যাকাউন্ট',   personal: true },
  { name: 'admin',       personal: true },
];

for (const host of ['node', 'static']) {
  console.log(`--- hosting: ${host} ---`);
  for (const p of pages) console.log(`${p.name}: ${choose(p, host)}`);
}

আউটপুট:

--- hosting: node ---
হোম: PPR: সবার shell + ব্যক্তিগত গর্ত
পণ্যের পাতা: PPR: cache-করা পাতা + live গর্ত
আমাদের কথা: SSG
ব্লগ তালিকা: ISR: লম্বা window + on-demand revalidation
কার্ট: Dynamic: প্রতি অনুরোধে, Cache-Control private
অ্যাকাউন্ট: Dynamic: প্রতি অনুরোধে, Cache-Control private
admin: Dynamic: প্রতি অনুরোধে, Cache-Control private
--- hosting: static ---
হোম: SSG + বদলানো অংশ ব্রাউজারে, আলাদা API থেকে
পণ্যের পাতা: SSG + বদলানো অংশ ব্রাউজারে, আলাদা API থেকে
আমাদের কথা: SSG
ব্লগ তালিকা: SSG: বদলালে আবার build ও upload
কার্ট: CSR: ব্রাউজারে আঁকা, ডেটা আলাদা API থেকে
অ্যাকাউন্ট: CSR: ব্রাউজারে আঁকা, ডেটা আলাদা API থেকে
admin: CSR: ব্রাউজারে আঁকা, ডেটা আলাদা API থেকে

একই দোকান, দুই hosting, দুই নকশা। Node সার্ভার থাকলে ব্যক্তিগত অংশ সার্ভারেই তৈরি হয়। Static hosting-এ সেই কাজ যায় ব্রাউজারে, আর ডেটা আসে আলাদা একটি API থেকে, যার নিরাপত্তা আর অনুমতির যাচাই পুরোপুরি সেই API-র দায়িত্ব। ছকটি ইচ্ছা করে সরল; যেমন admin-এ CSR shell আর API, অথবা প্রতি অনুরোধে সার্ভার render, দুটোই প্রচলিত।

পাতাকৌশল (Node সার্ভার)কেন, আর কী খেয়াল রাখবেন
হোম◐ PPR, বা আগের মডেলে ISR (১ ঘণ্টা)অফার আর ক্যাটাগরি সবার এক; “সালাম, রাফি” শুধু একটি Suspense-এর গর্তে। Layout-এ cookie পড়বেন না।
পণ্যের পাতাজনপ্রিয়গুলো build-এ, বাকি প্রথম অনুরোধে; লম্বা window আর দাম বদলালে on-demand; স্টক একটি গর্তেদেখানো দাম পুরোনো হতে পারে; অর্ডারের সময় দাম আর স্টক সার্ভারে ডেটাবেস থেকে আবার যাচাই বাধ্যতামূলক (পর্ব ৪৩)।
কার্টƒ dynamic, বা PPR-এ শিরোনাম shell আর সংখ্যা গর্তেকখনো ভাগ করা cache-এ নয়; header-এ private আছে কি না curl দিয়ে দেখুন।
অ্যাকাউন্টƒ dynamicSession যাচাই সার্ভারে (পর্ব ৪৫); অর্ডারের ইতিহাস শুধু নিজের।
adminƒ dynamic, বা CSR shell + APISEO লাগে না। প্রতিটি ডেটা আর কাজে অনুমতি সার্ভারে যাচাই (পর্ব ৪৮)। দাম বদলানোর কাজই on-demand revalidation ডাকবে।

আরও গভীরে: stale-while-revalidate নামটি Next.js-এর আবিষ্কার নয়; এটি HTTP-র একটি Cache-Control extension, যা Mark Nottingham ২০১০ সালে RFC 5861-এ প্রস্তাব করেন। ISR মূলত সেই ধারণাকে পাতা তৈরির স্তরে আনে। HTTP caching-এর মূল নিয়ম, যেমন s-maxage আর ভাগ করা বনাম ব্যক্তিগত cache, এখন RFC 9111-এ (২০২২)। PPR-এর stream-এ যে B:0 আর S:0 চিহ্ন দেখলেন, তা React-এর streaming SSR-এর ভেতরের কৌশল: আগে fallback-সহ জায়গা রাখা, পরে লুকানো div-এ আসল অংশ পাঠিয়ে script দিয়ে বসানো। এগুলো ভেতরের খুঁটিনাটি, সংস্করণ ভেদে বদলাতে পারে; এর ওপর নির্ভর করে কোড লিখবেন না।

পরীক্ষা ও ইন্টারভিউতে:

  • generateStaticParams কী করে? Build-এর সময় dynamic route-এর কোন params-এর পাতা বানাতে হবে তার তালিকা দেয়।
  • revalidate = 60 মানে কি প্রতি মিনিটে নতুন পাতা? না; ৬০ সেকেন্ড পরের প্রথম অনুরোধ পুরোনো পাতা পায় আর পেছনে নতুন তৈরি চালু করে।
  • কোন API পাতাকে dynamic করে? cookies(), headers(), searchParams, connection(), আর no-store fetch.
  • PPR এক বাক্যে? একই পাতার static shell আগে তৈরি থাকে আর সঙ্গে সঙ্গে যায়, dynamic অংশ Suspense-এর ভেতর থেকে stream হয়।
  • Static export-এ কী চলে না? Server Action, ISR, cookie, proxy, rewrite-redirect, আর তালিকা ছাড়া dynamic route.

১০ · AI-এর যুগে rendering কৌশল যাচাই

Next.js-এর rendering আর cache-এর API গত কয়েক বছরে যত দ্রুত বদলেছে, ওয়েবের অন্য খুব কম জায়গায় তা হয়েছে। AI-এর প্রশিক্ষণের ডেটায় সব সংস্করণের কোড মিশে আছে, তাই সে প্রায়ই তিন বছর আগের API আর আজকের API একই ফাইলে মিশিয়ে দেয়। কোড চলেও যেতে পারে, কিন্তু কৌশল হবে আপনার ভাবনার উল্টো।

AI-এর ভুলকেন সমস্যাযাচাইয়ের উপায়
App Router-এ getStaticProps বা getServerSidePropsএগুলো Pages Router-এর APIনিজের সংস্করণের নথি; build চালান
experimental.ppr: trueNext.js 16-এ সরানো; এখন cacheComponentsBuild-এর শুরুর লাইন আর route table দেখুন
cacheComponents-এর সাথে export const revalidate বা runtimeনথি অনুযায়ী এই মডেলে route-এর এসব config চলে না'use cache' আর cacheLife-এর নথি পড়ুন
ISR দেখাতে output: 'export'-এর সাথে revalidateBuild সফল হয়, কিন্তু ISR চুপচাপ উপেক্ষিতRoute table-এ Revalidate কলাম আছে কি না
Revalidate route-এ secret ছাড়াযে কেউ অকারণ render চালিয়ে সার্ভার ব্যস্ত রাখতে পারেCode review; ভুল secret দিয়ে নিজে ডেকে 401 দেখুন
Layout-এ cookies(), কারণ “সহজ”আগের মডেলে পুরো সাইট dynamic হয়ে যায়আগে-পরে route table তুলনা
“Edge-এ চালালে দ্রুত” বলে runtime = 'edge'লেখার সময়ের নথিতে deprecated; ডেটাবেস দূরে হলে ধীরও হতে পারেনথি দেখুন; দুই অবস্থায় TTFB মাপুন

AI-কে “ISR করে দাও” না বলে নিজের সংস্করণ, মডেল, পাতার চাহিদা আর যাচাইয়ের প্রমাণ চাইলে উত্তর অনেক ভালো হয়:

আমি Next.js 16.4 App Router ব্যবহার করছি, next.config-এ cacheComponents: true আর partialPrefetching: true. একটি অনলাইন দোকানের পণ্যের পাতা বানাচ্ছি: - নাম, ছবি, দাম সবার জন্য এক; দাম দিনে কয়েকবার বদলায়, admin প্যানেল থেকে। - "স্টকে কতটি" প্রতি অনুরোধে নতুন চাই। - জনপ্রিয় ২০০টি পণ্য build-এ বানাতে চাই, বাকিগুলো প্রথম অনুরোধে। শুধু এই সংস্করণের API ব্যবহার করো; কোনো API নিয়ে নিশ্চিত না হলে বলো, বানিয়ে লিখো না। Pages Router-এর API, experimental.ppr বা runtime export দিও না। বলো: কোন অংশ static shell-এ যাবে, কোনটি Suspense-এর গর্তে, next build-এর route table-এ কোন চিহ্ন আশা করব, আর দাম বদলালে কোন ফাংশন ডাকতে হবে। নতুন package যোগ করো না। কোনো secret দরকার হলে environment variable-এর নাম বলো, মান বানিয়ে লিখো না।

AI-এর লেখা rendering কোড হাতে পেলে

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১ISR জেনেশুনে ক্রেতাকে পুরোনো দাম দেখায়। এটি কি এক ধরনের ফাঁকি, নাকি একটি যুক্তিসঙ্গত চুক্তি? কোথায় এর সীমা টানবেন?

প্রশ্নটি আসলে পুরোনো: একটি ব্যবস্থা কি “সঠিক কিন্তু ধীর” হবে, নাকি “দ্রুত কিন্তু কিছুক্ষণ পুরোনো”? Distributed system-এ একে বলে consistency আর availability বা latency-র টানাপোড়েন। ওয়েবের HTTP cache শুরু থেকেই দ্বিতীয় পথে হেঁটেছে। Mark Nottingham ২০১০ সালে RFC 5861-এ stale-while-revalidate প্রস্তাব করেন ঠিক এই যুক্তিতে: পুরোনো উত্তর এখনই দিয়ে পেছনে নতুন আনলে ব্যবহারকারী অপেক্ষা থেকে বাঁচেন, আর বেশিরভাগ ক্ষেত্রে কয়েক সেকেন্ড পুরোনো তথ্যে কোনো ক্ষতি হয় না।

তবে “বেশিরভাগ ক্ষেত্রে” কথাটিই সীমা। আমাদের run-এ দাম বদলের পর 12 s ধরে পুরোনো দাম দেখা গেছে। পণ্যের বিবরণ বা ব্লগের লেখায় এটি নিরীহ। কিন্তু ক্রেতা যদি 450 টাকা দেখে কার্টে যোগ করেন আর বিল আসে 470, তাহলে সেটি আস্থার প্রশ্ন, আর কিছু ক্ষেত্রে ভোক্তা অধিকারেরও প্রশ্ন হতে পারে।

তাই সমাধান দুই স্তরে। দেখানোর স্তরে ISR চলতে পারে, কিন্তু টাকার স্তরে কখনো নয়: অর্ডার নেওয়ার মুহূর্তে সার্ভার ডেটাবেস থেকে দাম আর স্টক আবার পড়বে, আর পার্থক্য থাকলে ক্রেতাকে স্পষ্ট জানাবে। এটি পর্ব ৪৩-এর transaction-এর কাজ। দ্বিতীয়ত, দাম বদলের মতো জানা ঘটনায় সময়ের অপেক্ষা না করে on-demand revalidation ডাকুন, যাতে পুরোনো থাকার সময় প্রায় শূন্যে নামে। তৃতীয়ত, যে তথ্য সত্যিই এই মুহূর্তের হতে হবে, যেমন ফ্ল্যাশ সেলের বাকি সংখ্যা, তা একটি dynamic গর্তে রাখুন।

আরেকটি সূক্ষ্ম দিক: পুরোনো থাকার সর্বোচ্চ সময় আপনি জানেন কি না। revalidate একটি ঊর্ধ্বসীমা দেয় না, কারণ নতুন হওয়া শুরু হয় অনুরোধে; কম দর্শকের পাতা অনেক দিন পুরোনো থাকতে পারে, যতক্ষণ না কেউ আসেন। আবার নতুন render ব্যর্থ হলে নথি অনুযায়ী শেষ সফল পাতাই চলতে থাকে।

মূল উপলব্ধি: পুরোনো তথ্য দেখানো ফাঁকি নয়, যদি টাকা আর অনুমতির সিদ্ধান্ত সবসময় নতুন তথ্যে হয় আর পুরোনো থাকার সময় আপনি জেনেশুনে বেছে নেন।

প্র ০২আগের মডেলে Next.js “যা পারা যায় static” ধরে নিত। Cache Components-এ ডিফল্ট উল্টে “ডেটা প্রতি অনুরোধে, cache করতে হবে স্পষ্ট করে” হলো। কেন একটি framework এত বড় দার্শনিক বদল করল?

App Router স্থিতিশীল হয় Next.js 13.4-এ (২০২৩)। তখন ধারণা ছিল ডেভেলপার কিছু না বললেও framework যতটা পারে cache করবে, কারণ cache মানে দ্রুত আর সস্তা। বাস্তবে এতে একটি বড় সমস্যা দেখা দিল: অনেক ডেভেলপার বুঝতেই পারতেন না তাঁদের পাতা কেন পুরোনো দেখাচ্ছে, বা কেন একটি cookies() পুরো সাইটকে dynamic করে দিল। Community-র আলোচনায় “Next.js caching বিভ্রান্তিকর” অভিযোগ বারবার এসেছে। Next.js 15-এ (২০২৪) fetch-এর ডিফল্ট cache সরিয়ে নেওয়া হয়, যা এই প্রতিক্রিয়ারই একটি ফল বলে ব্যাপকভাবে দেখা হয়।

Cache Components এই ধারা শেষ পর্যন্ত নিয়ে যায়। নীতি হলো: অদৃশ্য cache-এর চেয়ে স্পষ্ট cache ভালো। ডেটা ডিফল্টে প্রতি অনুরোধে আসে, তাই “কেন পুরোনো” প্রশ্ন কম ওঠে। Cache চাইলে 'use cache' লিখতে হয়, আর cacheLife দিয়ে আয়ু বলতে হয়, তাই কোড পড়েই বোঝা যায় কোন অংশ জমা থাকবে। একই সাথে framework একটি শর্ত চাপায়: যে অংশ build-এ শেষ হয় না, তাকে হয় cache করতে হবে, নয়তো Suspense-এর ভেতরে রাখতে হবে। ফলে প্রতিটি route-এর একটি static shell থাকে, আর build বা dev overlay ভুল ধরিয়ে দেয়।

এই বদলের দামও আছে। পুরোনো কোড আর টিউটোরিয়াল মিলছে না, revalidate-এর মতো পরিচিত config এই মডেলে চলে না, আর ডেভেলপারকে নতুন ধারণা শিখতে হচ্ছে। কেউ কেউ বলেন framework-টি প্রতি বড় সংস্করণে মানসিক মডেল বদলাচ্ছে, যা দীর্ঘমেয়াদি প্রজেক্টের জন্য ক্লান্তিকর। অন্যরা বলেন এবারের মডেলটি অবশেষে সৎ: যা দেখেন, তাই হয়। দুই পক্ষের কথাতেই সত্য আছে, আর আপনার প্রজেক্টে কোনটি চালু তা না জেনে কোনো টিউটোরিয়াল অনুসরণ করা বিপজ্জনক।

মূল উপলব্ধি: Cache-এর সবচেয়ে বড় খরচ CPU নয়, বোঝার খরচ। যে নকশায় cache কোডে স্পষ্ট দেখা যায়, তা ভুল কম করায়, যদিও শুরুতে বেশি লিখতে হয়।

প্র ০৩Static export করে cPanel-এ তুলে দিলে খরচ প্রায় শূন্য। তাহলে সব প্রজেক্ট এভাবে কেন নয়? কোন প্রজেক্টের জন্য এটি সঠিক, আর কখন এটি একটি ফাঁদ?

Static সাইটের ধারণা নতুন নয়। ২০০৮ সালের দিকে Tom Preston-Werner-এর Jekyll আর GitHub Pages জনপ্রিয় করে তোলে “আগে HTML বানাও, তারপর যেকোনো সার্ভারে রাখো” পদ্ধতি। এর সুবিধা আজও একই: সার্ভারে কোনো কোড চলে না, তাই আক্রমণের পথ কম, ভেঙে পড়ার কিছু নেই, আর খুব সস্তা hosting-এও হাজার দর্শক সামলানো যায়। বাংলাদেশের অনেক ছোট ব্যবসার জন্য, যাঁদের আগে থেকেই cPanel hosting আছে, এটি বাস্তব সুবিধা।

কোন প্রজেক্টে সঠিক? যেখানে বিষয়বস্তু সবার জন্য এক, আর বদলায় কম: কোম্পানির পরিচিতি সাইট, ডকুমেন্টেশন, ব্লগ, পোর্টফোলিও, ছোট ক্যাটালগ যেখানে দাম মাসে কয়েকবার বদলায়। আমাদের পুরো তিন-পণ্যের দোকান ছিল মাত্র 766 KB আর ৪৪টি ফাইল।

কখন ফাঁদ? যখন প্রজেক্টটি আসলে একটি অ্যাপ। লগইন, অর্ডার, পেমেন্ট, ব্যক্তিগত তথ্য, এগুলোর জন্য কোথাও একটি সার্ভার লাগবেই। Static export-এ সেই সার্ভার আলাদা করে বানাতে হয়, যেমন একটি Express বা PHP API. তখন একটি অ্যাপের জায়গায় দুটি জিনিস চালাতে হয়, আর সব অনুমতির যাচাই সেই API-তে। দ্বিতীয় ফাঁদ নীরব ব্যর্থতা: আমাদের run-এ revalidate দিলেও build সফল হয়েছে, অথচ ISR কাজ করে না। কেউ যদি ভাবেন দাম নিজে নিজে ঠিক হবে, তিনি ভুল দাম নিয়ে দিন কাটাবেন। তৃতীয়ত, হাজার হাজার পণ্য আর ঘন ঘন দাম বদলে প্রতিবার পুরো build আর upload দ্রুত অসহনীয় হয়, যা খরচের মডেলে দেখেছেন।

মাঝামাঝি পথও আছে: CI-তে স্বয়ংক্রিয় build আর upload, বা কিছু hosting-এর Node অ্যাপ চালানোর সুবিধা, যদিও তার সীমা প্রদানকারী ভেদে আলাদা। সিদ্ধান্তের আগে প্রজেক্টের পরের দুই বছর ভাবুন, শুধু আজকের নয়।

মূল উপলব্ধি: Static export একটি সাইটের জন্য চমৎকার, একটি অ্যাপের জন্য অর্ধেক সমাধান। প্রশ্নটি হলো: আপনার প্রজেক্টের কোন অংশে সার্ভারকে সিদ্ধান্ত নিতে হয়?

প্র ০৪কয়েক বছর আগে “কোড edge-এ চালাও, ব্যবহারকারীর কাছে” ছিল আধুনিক ওয়েবের বড় প্রতিশ্রুতি। আজ Next.js-এর নথি Edge runtime-কে deprecated বলছে। এই উল্টো যাত্রার পেছনে কী যুক্তি থাকতে পারে?

প্রতিশ্রুতিটি যুক্তিসঙ্গত শোনাত। আলো একটি নির্দিষ্ট গতিতে চলে, তাই ঢাকার ব্যবহারকারীর জন্য দূরের অঞ্চলের সার্ভারে প্রতিটি অনুরোধে দূরত্বের দাম দিতে হয়। CDN-এর edge-এ static ফাইল রাখলে এই দাম কমে, এটি প্রমাণিত। তাই ভাবা হলো, কোডও যদি সেখানে চলে, তাহলে dynamic পাতাও একই রকম দ্রুত হবে। হালকা runtime-এর আরেকটি সুবিধা ছিল দ্রুত চালু হওয়া।

সমস্যা দেখা দিল ডেটাবেসে। বেশিরভাগ পাতার আসল কাজ ডেটা আনা, আর ডেটাবেস সাধারণত এক অঞ্চলে থাকে। ধরুন পাতাটি তিনটি query চালায়, একটির পর একটি। কোড ডেটাবেসের পাশে চললে প্রতিটি query কয়েক millisecond. কোড ব্যবহারকারীর কাছে আর ডেটাবেস দূরে থাকলে প্রতিটি query-কে সেই দূরত্ব পার হতে হয়, তিনবার। ফলে মোট সময় বরং বাড়তে পারে। এর সাথে যোগ হলো সীমাবদ্ধতা: অনেক ডেটাবেস driver আর Node module edge-এ চলে না, তাই বিশেষ driver বা HTTP-ভিত্তিক ডেটাবেস সেবা লাগে। Community-তে বহু লেখায় এই অভিজ্ঞতা এসেছে, যদিও প্রতিটি অ্যাপের ফল আলাদা।

লেখার সময়ের Next.js নথি কারণ বিস্তারিত বলে না, শুধু জানায় মানটি deprecated আর Node.js ডিফল্ট; Cache Components আর ISR Node.js runtime চায়। কাজেই আমরা কারণ নিয়ে যা বলছি, তা যুক্তিসঙ্গত অনুমান, অফিসিয়াল ঘোষণা নয়। Framework-এর দিক থেকে আরেকটি ব্যাখ্যা সম্ভব: PPR এসে “static shell edge থেকে, বাকিটা ডেটার কাছে” দেওয়ার পথ খুলে দিয়েছে, যা দুই জগতের সুবিধা একসাথে দিতে চায়। অর্থাৎ edge-এ কোড না পাঠিয়ে edge-এ HTML পাঠানো।

এখান থেকে সাধারণ একটি শিক্ষা বেরোয়: latency কমানোর প্রশ্নে আগে খুঁজুন অনুরোধের সময় কোন দুটি জিনিস বারবার কথা বলে, তারপর তাদের কাছাকাছি রাখুন। System Design-এর latency পর্বে এই হিসাব আছে।

মূল উপলব্ধি: “ব্যবহারকারীর কাছে” সবসময় দ্রুত নয়; দ্রুত হলো “যে জিনিসের সাথে সবচেয়ে বেশি কথা বলতে হয়, তার কাছে”। Static অংশের জন্য সেটি ব্যবহারকারী, ডেটার কাজের জন্য সেটি ডেটাবেস।

প্র ০৫আমাদের মাপে static পাতার TTFB 1.8 ms আর dynamic পাতার 66 ms এসেছে। কেউ লিখলেন, “Static ৩৫ গুণ দ্রুত।” এই দাবিতে কী কী গোলমাল আছে, আর সঠিকভাবে মাপতে কী করবেন?

সংখ্যাগুলো সত্যি, কিন্তু দাবিটি অনেক বেশি সাধারণ। প্রথম গোলমাল পরিবেশ: ক্লায়েন্ট আর সার্ভার একই মেশিনে, তাই নেটওয়ার্কের দূরত্ব প্রায় শূন্য। আসল ব্যবহারকারীর কাছে DNS, TCP, TLS আর দূরত্ব মিলে হয়তো একশো বা কয়েকশো millisecond যোগ হবে, যা দুই পাতাতেই প্রায় সমান। তখন অনুপাত ৩৫ গুণ থেকে নেমে অনেক কম হবে, যদিও পার্থক্যটি থেকে যাবে।

দ্বিতীয় গোলমাল কারণ: dynamic পাতার 66 ms-এর 50 ms আমরা ইচ্ছা করে রেখেছি। আসল ডেটাবেস 5 ms-এ উত্তর দিলে পার্থক্য অনেক কম; ধীর query বা দূরের ডেটাবেসে অনেক বেশি। অর্থাৎ মাপটি “static বনাম dynamic” নয়, “ডেটাবেসের অপেক্ষা আছে বনাম নেই”।

তৃতীয় গোলমাল পরিসংখ্যান: ২১টি নমুনার median একটি কেন্দ্রীয় মান দেয়, কিন্তু ধীরতম ব্যবহারকারীর কথা বলে না। আমাদের run-এই হোমের একটি অনুরোধ 12.7 ms নিয়েছে, median-এর ছয় গুণ। বাস্তবে p95 বা p99 percentile দেখা দরকার, আর মাঠের ডেটায় Core Web Vitals মাপা হয় p75 ধরে। চতুর্থত, সার্ভার ঠান্ডা না গরম, cache-এ আছে কি না, একসাথে কতজন অনুরোধ করছেন, এগুলোও ফল বদলায়। আমাদের PPR run-এ প্রথম অনুরোধে header এসেছিল 23 ms-এ, দ্বিতীয়তে 5 ms-এ।

সঠিক পথ: একই পরিবেশে বারবার মেপে median আর উঁচু percentile দুটোই লিখুন; সংস্করণ, মেশিন আর সেটিং উল্লেখ করুন; সম্ভব হলে আসল নেটওয়ার্কে, আসল ডেটাবেসে আর চাপের মধ্যে মাপুন; আর শেষ পর্যন্ত আসল ব্যবহারকারীর মাঠের ডেটা দেখুন। TTFB-র পাশাপাশি LCP-ও দেখুন, কারণ PPR-এ TTFB কম হলেও মূল বিষয়বস্তু গর্তে থাকলে LCP দেরিতে আসতে পারে।

মূল উপলব্ধি: একটি local মাপ একটি প্রশ্নের উত্তর দেয়, সব প্রশ্নের নয়। সংখ্যার সাথে সবসময় লিখুন কোথায়, কীভাবে, কতবার মেপেছেন।

অনুশীলন

হিসাব, ফল অনুমান, নিজে চালানো আর নকশার সমালোচনা, চার ধরনের কাজ।

  1. হিসাব: একটি দোকানে ১২,০০০ পণ্য। একটি পাতা build করতে গড়ে 30 ms CPU লাগে, আর build মেশিনে ৬টি worker সমান্তরালে চলে। (ক) সব পাতা generateStaticParams-এ দিলে শুধু পাতা তৈরিতে কত সময় লাগবে? (খ) শুধু জনপ্রিয় ৬০০টি দিলে? (গ) জনপ্রিয় ৬০০ পাতায় revalidate = 300 হলে, আর প্রতিটিতে দিনে অনেক দর্শক এলে, দিনে সর্বোচ্চ কতবার পেছনের render হতে পারে?

    (ক) মোট CPU 12000 × 30 = 360000 ms = 360 s. ৬টি worker-এ 360 ÷ 6 = 60 s, অর্থাৎ প্রায় ১ মিনিট (compile আর অন্যান্য কাজ বাদে)।

    (খ) 600 × 30 = 18000 ms = 18 s, ৬ worker-এ 3 s.

    (গ) একটি পাতা দিনে সর্বোচ্চ 86400 ÷ 300 = 288 বার। ৬০০ পাতায় 600 × 288 = 172800 বার। CPU-তে 172800 × 30 ms = 5184 s ≈ 1.44 ঘণ্টা। দর্শক কম হলে আসল সংখ্যা এর চেয়ে কম, কারণ নতুন হওয়া শুরু হয় অনুরোধ থেকে।

  2. ফল অনুমান করুন: একটি Next.js পাতায় (আগের মডেল) export const revalidate = 30. পাতাটি t = 0-এ তৈরি হয়েছে, দাম 200. t = 10 s-এ ডেটাবেসে দাম 250 হলো। অনুরোধ এল t = 20, t = 35, t = 36 (পেছনের render শেষ হওয়ার পরে) সেকেন্ডে। তারপর t = 40-এ দাম 300 হলো আর সঙ্গে সঙ্গে সেই পাতার জন্য revalidatePath ডাকা হলো, আর t = 41 ও t = 42-এ অনুরোধ এল। প্রতিটি অনুরোধের x-nextjs-cache আর দেখানো দাম কী?
    • t = 20: HIT, 200. পাতার বয়স 20 s, window 30 s-এর ভেতরে।
    • t = 35: STALE, 200. বয়স 35 s; পুরোনো পাতা গেল, পেছনে render শুরু, যা দাম 250 পড়বে।
    • t = 36: HIT, 250.
    • t = 41: MISS, 300. revalidatePath পাতা বাতিল করেছে, তাই এই অনুরোধ অপেক্ষা করে নতুন render পেল। সত্যিকারের run-এও ঠিক এই ক্রম দেখা গেছে।
    • t = 42: HIT, 300.
  3. নিজে চালান: npx create-next-app@latest দিয়ে একটি প্রজেক্ট বানান, তারপর next.config খুলে দেখুন cacheComponents আছে কি না। একটি পাতায় await cookies() লিখুন Suspense ছাড়া, তারপর npx next build চালান। কী ঘটে? এরপর অংশটিকে <Suspense>-এর ভেতরে নিয়ে আবার build করুন, আর route table-এর চিহ্ন তুলনা করুন। শেষে npx next start -p 3600 চালিয়ে আরেক terminal-এ curl -sI http://127.0.0.1:3600/ দিয়ে header দেখুন।

    create-next-app@16.4.4 --yes-এর সত্যিকারের run-এ next.config.ts-এ cacheComponents: true আর partialPrefetching: true ছিল; প্রশ্নের উত্তর দিয়ে বা অন্য সংস্করণে আলাদা হতে পারে, তাই config ফাইলই প্রমাণ। চালু থাকলে Suspense ছাড়া cookies() পড়লে নথি অনুযায়ী dev overlay আর build “blocking-route” ধরনের সতর্কতা বা ভুল দেখায় এবং সমাধান হিসেবে Suspense-এ মোড়ানোর কথা বলে। Suspense-এ নেওয়ার পর পাতাটি ◐ চিহ্ন পায়। চালু না থাকলে পাতাটি সোজা ƒ হয়, কোনো ভুল ছাড়াই। Header-এ dynamic বা ◐ পাতার জন্য private, no-store আশা করুন। আপনার আউটপুট সংস্করণ ভেদে একটু আলাদা হতে পারে; এখানে কোনো বানানো আউটপুট দেওয়া হলো না, নিজের run-ই উত্তর।

  4. নকশার সমালোচনা: একজন ফ্রিল্যান্সার “বাজারঘর”-এর জন্য প্রস্তাব দিলেন: “ক্লায়েন্টের cPanel hosting আছে, তাই output: 'export' করব। দাম যাতে নিজে নিজে ঠিক থাকে, সব পণ্যের পাতায় revalidate = 60 দেব। চেকআউটের form-এ Server Action, আর লগইনের পর হেডারে নাম দেখাতে root layout-এ cookies() পড়ব।” কী কী ভুল, আর আপনি কী প্রস্তাব করবেন?
    • revalidate export-এ কাজ করে না, আর build তা চুপচাপ উপেক্ষা করে। দাম বদলাতে নতুন build আর upload লাগবে, যা CI দিয়ে স্বয়ংক্রিয় করা যায়।
    • Server Action export-এ নেই; সত্যিকারের build “Server Actions are not supported with static export” বলে থামে। চেকআউটের জন্য আলাদা API লাগবে।
    • Layout-এ cookies() export-এ build ভাঙবে। Node সার্ভার থাকলেও আগের মডেলে পুরো সাইট dynamic করত। নাম দেখানো ব্রাউজারে, বা PPR-এর একটি গর্তে করুন।
    • বিকল্প ১: Static export রাখুন ক্যাটালগের জন্য, আর অর্ডার, লগইন, পেমেন্ট আলাদা একটি backend-এ, যেখানে দাম আর স্টক সার্ভারে যাচাই হবে।
    • বিকল্প ২: Node চালানো যায় এমন hosting নিন; তখন পণ্যের পাতা ISR বা PPR, কার্ট আর অ্যাকাউন্ট dynamic, আর Server Action ব্যবহার করা যায়। খরচ আর রক্ষণাবেক্ষণের তুলনা ক্লায়েন্টকে স্পষ্ট জানান।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমNext.js-এ সময়ভিত্তিক ISR-এর ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Build-এর সময় পাতা তৈরি হয়ে cache-এ যায়
  2. Window-এর ভেতরের অনুরোধ cache থেকে পায়, HIT
  3. Window পেরোনোর পর প্রথম অনুরোধ পুরোনো পাতা পায়, STALE
  4. সেই অনুরোধের কারণে পেছনে নতুন render হয়
  5. পরের অনুরোধ নতুন পাতা পায়, HIT

Next.js App Router-এর একটি পাতায় (Cache Components বন্ধ) export const revalidate = 60 আছে। ৬০ সেকেন্ড পেরোনোর পর এক ঘণ্টা কোনো দর্শক আসেননি। এই এক ঘণ্টায় কী ঘটে?

  • প্রতি ৬০ সেকেন্ডে পাতাটি নিজে থেকে নতুন হয়, মোট ৬০ বার
  • কিছুই ঘটে না; পরের দর্শক এলে পুরোনো পাতা পেয়ে পেছনে নতুন render চালু হবে
  • পাতাটি cache থেকে মুছে যায়, পরের দর্শক 404 পান
  • পুরো সাইট আবার build হয়

Next.js-এ (Cache Components বন্ধ) নিচের কোনটি একা একটি route-কে প্রতি অনুরোধে render হতে বাধ্য করে?

  • generateStaticParams ফাংশন রাখা
  • export const revalidate = 3600
  • একটি JSON ফাইল থেকে পণ্যের তালিকা import করা
  • Component-এ await cookies() পড়া

একটি ISR পাতার উত্তরে header এল: Cache-Control: s-maxage=10, stale-while-revalidate=31535990. এর সঠিক ব্যাখ্যা কোনটি?

  • CDN-এর মতো ভাগ করা cache 10 s তাজা ধরবে, তারপর প্রায় এক বছর পুরোনো কপি দিয়ে পেছনে নতুন আনতে পারবে
  • ব্রাউজার পাতাটি 10 s জমা রাখবে
  • পাতাটি কোনো cache-এ রাখা যাবে না
  • পাতাটি 10 দিন পর মুছে যাবে

Next.js 16.4-এ partial prerendering চালু করার স্বীকৃত উপায় কোনটি?

  • next.config-এ experimental.ppr: true
  • প্রতিটি পাতায় export const experimental_ppr = true
  • next.config-এ cacheComponents: true (সাথে partialPrefetching)
  • প্রতিটি পাতায় export const runtime = 'edge'

একটি Next.js 16 প্রজেক্টে output: 'export' দেওয়া আছে। নিচের কোনটি build ভাঙে না, কিন্তু চুপচাপ কাজও করে না?

  • একটি form-এ Server Action
  • পণ্যের পাতায় export const revalidate = 60
  • একটি পাতায় await cookies()
  • Client Component-এ useEffect-এর ভেতরে localStorage পড়া

একটি পণ্যের পাতায় partial prerendering: নাম আর দাম static shell-এ, স্টক আসে একটি API থেকে যার উত্তর দিতে 800 ms লাগে। এক local run-এ TTFB আর পুরো উত্তরের সময় সম্পর্কে কোনটি সবচেয়ে সম্ভাব্য?

  • TTFB প্রায় 800 ms, পুরো উত্তর প্রায় 800 ms
  • TTFB কয়েক ms, পুরো উত্তরও কয়েক ms
  • TTFB প্রায় 800 ms, পুরো উত্তর প্রায় 1600 ms
  • TTFB কয়েক ms, পুরো উত্তর প্রায় 800 ms

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Next.js কোড দেখে কৌশল ঠিক করে, আর next build-এর route table (○ ● ƒ ◐) সেই সিদ্ধান্তের প্রমাণ। প্রথমে নিজের প্রজেক্টের মডেল জানুন: cacheComponents চালু কি না।
  2. SSG-তে generateStaticParams, জনপ্রিয় পাতা build-এ, বাকি প্রথম অনুরোধে; না থাকা পণ্যে notFound().
  3. ISR পুরোনো কপি আগে দেয়, নতুন render শুরু হয় window-এর পরের প্রথম অনুরোধে। জানা বদলে revalidatePath, আর টাকার সিদ্ধান্ত সবসময় ডেটাবেসের নতুন তথ্যে।
  4. PPR মানে static shell আগে, Suspense-এর গর্ত পরে, একই উত্তরে। TTFB ধীর অংশের অপেক্ষা করে না।
  5. Static export সস্তা কিন্তু সার্ভারহীন: Server Action, ISR, cookie নেই। ব্যক্তিগত পাতা কখনো ভাগ করা cache-এ নয়, আর সব সংখ্যা মেপে, স্পষ্ট শর্তসহ লিখুন।

পরের পর্বে: সার্ভার HTML পাঠাল, এবার ব্রাউজারের পালা। Hydration-এ ঠিক কী ঘটে, সার্ভার আর ব্রাউজারের HTML না মিললে কী হয়, আর এর খরচ কীভাবে কমাবেন, তা পর্ব ৫৬-এ।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান