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

Data fetching আর cache — fetch, revalidate আর streaming

Data Fetching and Caching in Next.js 16 — use cache, Tags, Revalidation and Streaming

Taught in Bangla, this lesson fetches data directly in Server Components instead of calling your own API, fixes request waterfalls with Promise.all, dedupes with React cache(), explains the explicit Next.js 16 cache model with "use cache", cacheLife and cacheTag, refreshing with revalidateTag and updateTag, request APIs like cookies(), headers() and searchParams, streaming with loading.js and Suspense, error.js, and where TanStack Query still fits.

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

  • Server component-এ ডেটা আনার সঠিক জায়গা, আর নিজের API-কে ডাকা কখন অপচয়
  • Request waterfall চেনা, মাপা আর Promise.all দিয়ে ভাঙা; React cache() দিয়ে একই query একবার চালানো
  • Next.js 16-এর cache মডেল: "use cache", cacheLife, cacheTag, revalidateTag, updateTag, revalidatePath
  • cookies(), headers(), searchParams কীভাবে পাতাকে অনুরোধ-নির্ভর করে, আর <Suspense> দিয়ে কীভাবে stream হয়
  • বাজারঘরের পণ্য-তালিকা আর পণ্যের পাতা ধীর ডেটাবেসেও দ্রুত দেখানো, আর error.js দিয়ে ভাঙন সামলানো

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

১ · Server component-এ ডেটা আনা: সরাসরি ডেটাবেস, নাকি নিজের API

পর্ব ৫২-তে (Server Component আর Client Component) দেখেছেন, একটি Server Component শুধু server-এ চলে, আর তার কোড browser-এ যায় না। এর সবচেয়ে বড় সুবিধা ডেটা আনায়। Component নিজেই async হতে পারে। ভেতরে সরাসরি await করে ডেটাবেস পড়া যায়। useEffect, loading state বা আলাদা API লাগে না।

একটি রেস্তোরাঁর কথা ভাবুন। ওয়েটার (browser) রান্নাঘরে অর্ডার দেন। পুরোনো পথে রাঁধুনি (server-এ HTML বানানো অংশ) ভাঁড়ারঘরে নিজে না গিয়ে আরেকজন কর্মীকে (নিজের API) ফোন করতেন, আর সেই কর্মী ভাঁড়ার (ডেটাবেস) থেকে মাল এনে দিতেন। একই রান্নাঘরে থেকেও মাঝে একটি ফোনকল। Server Component মানে রাঁধুনি নিজেই ভাঁড়ারে হাত বাড়ান।

বাজারঘরের প্রকল্পে ডেটাবেস ইচ্ছা করে ধীর রাখা হয়েছে, যাতে সময় চোখে পড়ে। একটি JSON ফাইল পড়ার আগে প্রতিটি query নির্দিষ্ট সময় ঘুমায়: পণ্য-তালিকা ৩০০ ms, একটি পণ্য ২০০ ms, স্টক ৪০০ ms, রিভিউ ১,২০০ ms. প্রতিটি query একটি log লাইন লেখে, ফলে কখন ডেটাবেসে যাওয়া হলো তা গুনে দেখা যায়।

async function query<T>(label: string, ms: number, pick: (s: Shop) => T) {
  const t0 = performance.now();
  await sleep(ms);                                   // ধীর ডেটাবেসের অনুকরণ
  const shop: Shop = JSON.parse(await readFile(FILE, "utf8"));
  calls++;
  console.log(`[db #${calls}] ${label} ${Math.round(performance.now() - t0)}ms`);
  return pick(shop);
}
export const db = {
  listProducts: () => query("SELECT products", 300, (s) => s.products),
  getProduct: (id: number) => query(`SELECT product ${id}`, 200, (s) => s.products.find((p) => p.id === id) ?? null),
  getReviews: (id: number) => query(`SELECT reviews ${id}`, 1200, (s) => s.reviews[id] ?? []),
  getStock: (id: number) => query(`SELECT stock ${id}`, 400, (s) => s.stock[id] ?? 0),
};

এবার একই পাতায় দুইভাবে পণ্য-তালিকা আনা হলো: একবার সরাসরি db.listProducts(), আরেকবার নিজের /api/products route-কে fetch দিয়ে ডেকে। Route-টি ভেতরে আবার সেই একই query চালায়।

সত্যিকারের run · Next.js 16.4.4, next build তারপর next start · একটি local কম্পিউটারে একবার

[db #19] SELECT products 304ms
[direct] 3 products 304ms
[db #1] SELECT products 305ms
[via-api] 3 products 329ms

দুটি জিনিস লক্ষ করুন। প্রথমত, API পথে ২৫ ms বেশি লাগল। একই কম্পিউটারে HTTP অনুরোধ, JSON বানানো আর আবার পড়ার খরচ এটি। Server আলাদা যন্ত্রে বা অন্য region-এ থাকলে এই খরচ আরও বাড়ে। দ্বিতীয়ত, counter হঠাৎ #1 হয়ে গেছে। Route handler আর পাতা build-এর পর আলাদা bundle-এ থাকে, তাই module-এর ভেতরের calls ভ্যারিয়েবলও আলাদা। Module-এর ভেতরের ভ্যারিয়েবলে জমানো state তাই নির্ভরযোগ্য নয়। এজন্যই এই প্রকল্পে ডেটা ফাইলে রাখা হয়েছে।

মূল ধারণা

পাতা বানাতে ডেটা লাগলে Server Component থেকে সরাসরি ডেটা-স্তরের ফাংশন ডাকুন। নিজের Route handler রাখুন অন্যদের জন্য: মোবাইল অ্যাপ, বাইরের সেবা, webhook, বা browser থেকে আসা অনুরোধ। Route handler-এর বিস্তারিত পর্ব ৫৭-তে (Route handler আর middleware).

"সরাসরি ডেটাবেস" মানে নিরাপত্তা ভুলে যাওয়া নয়। Server Component-এ লেখা query browser-এ যায় না ঠিকই, কিন্তু কোন ব্যবহারকারী কোন ডেটা দেখতে পারবেন সেই যাচাই এখনো আপনার কাজ। Permission-এর নিয়ম পর্ব ৪৮-এ (Permission), আর সেই নিয়ম একটি data-access ফাংশনে রাখাই ভালো।

২ · Request waterfall বনাম Promise.all

পণ্যের পাতায় তিনটি জিনিস লাগে: পণ্য, স্টক আর রিভিউ। তিনটি একে অপরের ওপর নির্ভর করে না। তবু সহজ কোড এমন হয়:

const product = await db.getProduct(1);   // ২০০ ms
const stock = await db.getStock(1);       // ৪০০ ms, আগেরটি শেষ হলে শুরু
const reviews = await db.getReviews(1);   // ১২০০ ms, তারও পরে

প্রতিটি await থেমে থাকে আগেরটির জন্য। একে বলে request waterfall: সিঁড়ির মতো একটির পর একটি। সমাধান হলো সবগুলো একসাথে শুরু করা, তারপর একবারে অপেক্ষা করা। এটাই Promise.all:

const [product, stock, reviews] = await Promise.all([
  db.getProduct(1), db.getStock(1), db.getReviews(1),   // তিনটি একসাথে শুরু
]);

সত্যিকারের run · Next.js 16.4.4 · একটি local কম্পিউটারে একবার · server log আর পুরো পাতা আসার সময়

[db #13] SELECT product 1 201ms
[db #14] SELECT stock 1 411ms
[db #15] SELECT reviews 1 1216ms
[waterfall] 1829ms            পুরো পাতা: 1886 ms
[db #16] SELECT product 1 202ms
[db #17] SELECT stock 1 406ms
[db #18] SELECT reviews 1 1205ms
[parallel] 1206ms             পুরো পাতা: 1253 ms

একই তিনটি query, দুই রকম সময়রেখা

Sequential awaits add up; parallel awaits cost only the slowest one

Waterfall বনাম Promise.all-এর সময়রেখা Waterfall · মোট 1800 ms Promise.all · মোট 1200 ms product stock reviews 1200 product 200 stock 400 reviews 1200 1200 1800
ছবি ৫৩.১ — Waterfall-এ মোট সময় তিনটির যোগফল, একসাথে চালালে শুধু সবচেয়ে ধীরটি।

হিসাবের নিয়ম

ধরা যাক query-গুলোর সময় t₁, t₂, …, tₙ. একটির পর একটি চালালে মোট সময় Σtᵢ. স্বাধীন হলে একসাথে চালিয়ে মোট সময় max(tᵢ). কিছু query আগের ফলের ওপর নির্ভর করলে (যেমন আগে ব্যবহারকারী, তারপর তাঁর অর্ডার) সেই শিকলটুকু একসাথে করা যায় না। তখন মোট সময় হলো সবচেয়ে লম্বা নির্ভরতার পথ, যাকে বলে critical path.

২০০ + ৪০০ + ১২০০ = ১৮০০ ms (waterfall)
max(২০০, ৪০০, ১২০০) = ১২০০ ms (Promise.all) · সাশ্রয় ৩৩%

মাপা সংখ্যা হিসাবের খুব কাছে: ১,৮২৯ বনাম ১,২০৬ ms. বাড়তি কয়েক ms আসে timer আর ফাইল পড়া থেকে। নিচের মডেলে একটি নকল ঘড়ি আছে, তাই প্রতিবার হুবহু একই সংখ্যা আসে। এটি শেখানোর মডেল, Next.js নয়।

// শেখানোর মডেল: নকল ঘড়ি, তাই প্রতিবার একই সংখ্যা আসে
let now = 0, timers = [];
const sleep = (ms) => new Promise((r) => timers.push({ at: now + ms, r }));
const log = [];

async function query(name, ms) {
  log.push(`  t=${String(now).padStart(3)}ms start ${name}`);
  await sleep(ms);
  log.push(`  t=${String(now).padStart(3)}ms end   ${name}`);
  return name;
}
async function waterfall() {
  const p = await query('product', 200);   // শেষ হলে তবেই পরেরটি
  const s = await query('stock', 400);
  const r = await query('reviews', 1200);
  return [p, s, r];
}
async function parallel() {               // তিনটি একসাথে শুরু
  return Promise.all([query('product', 200), query('stock', 400), query('reviews', 1200)]);
}
async function run(label, fn) {           // নকল ঘড়ি চালানোর ছোট scheduler
  now = 0; timers = []; log.length = 0;
  let done = false;
  fn().then(() => { done = true; });
  while (!done) {
    await new Promise((r) => setImmediate(r));
    if (done || !timers.length) break;
    timers.sort((a, b) => a.at - b.at);
    const t = timers.shift(); now = t.at; t.r();
  }
  console.log(`${label}: total ${now} ms\n${log.join('\n')}`);
}
run('waterfall', waterfall).then(() => run('Promise.all', parallel));

প্রথম অংশে waterfall: total 1800 ms, আর প্রতিটি start আগেরটির end-এর সময়েই: t=200ms start stock, t=600ms start reviews. দ্বিতীয় অংশে Promise.all: total 1200 ms, আর তিনটি start-ই t= 0ms-এ। এখানে মূল কথা হলো Promise তৈরি হওয়ামাত্র কাজ শুরু হয়। Promise.all শুধু সবগুলোর জন্য একসাথে অপেক্ষা করে। তাই const a = db.getStock(1) লিখে পরে await a করলেও কাজ আগে থেকেই চলতে থাকে।

Component-এর গাছেও waterfall হয়। ধরুন <Page> নিজে await করে, তারপর ভেতরের <Reviews> আবার await করে। ভেতরেরটি শুরুই হবে বাইরেরটি শেষ হলে। এর প্রতিকার দুটি: ডেটা ওপরে একসাথে শুরু করে Promise নিচে পাঠানো, অথবা প্রতিটি ধীর অংশকে আলাদা <Suspense>-এ রাখা যাতে সেগুলো নিজের মতো চলে (অংশ ৭)। আর লুপের ভেতরে await যে N+1 সমস্যা তৈরি করে, তা পর্ব ৪২-এ (N+1 সমস্যা) দেখেছেন।

৩ · React cache(): একটি অনুরোধে একই query একবার

বড় পাতায় একই ডেটা অনেক জায়গায় লাগে। পণ্যের ক্যাটাগরির নাম লাগে header-এ, breadcrumb-এ আর sidebar-এ। তিনটি component তিনবার ডাকলে ডেটাবেসে তিনটি query যায়। Prop দিয়ে নিচে পাঠানো যায়, কিন্তু তাতে component-গুলো জড়িয়ে যায়।

React একটি ছোট সমাধান দেয়: React cache(). একটি ফাংশনকে cache() দিয়ে মুড়ে দিলে একটি server অনুরোধের ভেতরে একই argument-এ দ্বিতীয়বার ডাকলে আগের ফলটিই ফেরে। অনুরোধ শেষ হলে সেই স্মৃতি মুছে যায়। এটি deduplication, স্থায়ী cache নয়।

import { cache } from "react";
// একটি অনুরোধের ভেতরে একবারই ডেটাবেস
export const getCategoryOnce = cache((id: number) => db.getCategory(id));

পরীক্ষার পাতায় তিনটি component সাধারণ db.getCategory(5) ডাকল, আর তিনটি ডাকল getCategoryOnce(5):

সত্যিকারের run · Next.js 16.4.4, React 19.3.0 · একটি local কম্পিউটারে একবার · একটি অনুরোধ

[db #20] SELECT category 5 104ms
[db #21] SELECT category 5 104ms
[db #22] SELECT category 5 104ms
[db #23] SELECT category 5 104ms

ছয়বার ডাকা হলো, query গেল চারটি: সাধারণ পথের তিনটি, আর cache()-এর পথে একটি। Next.js নিজের fetch-এর ক্ষেত্রেও একই কাজ করে: একই render-এ একই URL আর একই option-এর GET একবারই যায়। একে Next.js-এর docs বলে memoization. কিন্তু ORM বা নিজের ডেটাবেস ফাংশনের জন্য cache() আপনাকেই দিতে হয়।

ভেতরে কী ঘটে, নিচের মডেলে দেখুন। প্রতিটি অনুরোধ নিজের একটি Map পায়। Node-এর AsyncLocalStorage সেই Map-কে অনুরোধের সব async কাজের সাথে বেঁধে রাখে। এটি শেখানোর মডেল; React-এর ভেতরের কোড এমন নয়, তবে আচরণ একই।

// শেখানোর মডেল: React cache()-এর মতো, প্রতিটি অনুরোধের নিজের Map
const { AsyncLocalStorage } = require('node:async_hooks');
const requestScope = new AsyncLocalStorage();
let dbHits = 0;

async function getCategory(id) {
  dbHits++;
  await new Promise((r) => setTimeout(r, 5));      // ধীর ডেটাবেস
  return id === 5 ? 'grocery' : '?';
}
function cache(fn) {
  return (...args) => {
    const memo = requestScope.getStore();           // এই অনুরোধের Map
    if (!memo) return fn(...args);                  // অনুরোধের বাইরে: dedupe নেই
    const key = JSON.stringify(args);
    if (!memo.has(key)) memo.set(key, fn(...args)); // Promise-টাই জমা থাকে
    return memo.get(key);
  };
}
const getCategoryOnce = cache(getCategory);

function renderPage(reqId, f) {
  return requestScope.run(new Map(), async () => {
    const before = dbHits;
    const parts = await Promise.all([f(5), f(5), f(5)]); // header, breadcrumb, sidebar
    console.log(`req ${reqId}: ${parts.join(',')}  db queries=${dbHits - before}`);
  });
}
(async () => {
  await renderPage(1, getCategory);
  await renderPage(2, getCategoryOnce);
  await renderPage(3, getCategoryOnce);             // নতুন অনুরোধ, নতুন Map
  await getCategoryOnce(5); await getCategoryOnce(5);
  console.log(`outside a request: total db queries=${dbHits}`);
})();

req 1 … db queries=3: মোড়ানো নেই, তাই তিনবার। req 2 … db queries=1: তিনটি ডাক একটিই Promise ভাগ করল। খেয়াল করুন, ফল নয়, Promise জমা হয়েছে। তাই তিনটি ডাক একসাথে এলেও query একটিই যায়। req 3 … db queries=1: নতুন অনুরোধ নতুন Map পায়, আগের অনুরোধের ফল এখানে কাজে আসে না। শেষ লাইন total db queries=7: ৩ + ১ + ১, আর অনুরোধের বাইরে দুটি ডাক কোনো dedupe পায়নি, তাই আরও ২. React-এর আসল cache()-ও শুধু Server Component render-এর ভেতরে কাজ করে।

৪ · Next.js 16-এর cache: "use cache" আর cacheLife

পুরোনো টিউটোরিয়াল সাবধানে পড়ুন। Next.js-এর cache-এর default কয়েকবার বদলেছে। App Router-এর শুরুর দিকের সংস্করণে (13, 14) server-এর fetch নিজে থেকেই cache হতো, আর অনেকে অবাক হতেন কেন নতুন ডেটা আসছে না। Next.js 15 সেই default উল্টে দেয়: fetch আর cache হয় না, চাইলে cache: 'force-cache' লিখতে হয়। Next.js 16 আনে Cache Components: cacheComponents: true দিলে cache পুরোপুরি স্পষ্ট, "use cache" দিয়ে। লেখার সময়ের stable সংস্করণ 16.4.4, আর তার create-next-app-এর নতুন প্রকল্পে এটি আগে থেকেই চালু ছিল। export const revalidate = 60, unstable_cache বা fetch(…, { next: { revalidate } }) দেখলে বুঝবেন সেটি আগের মডেল, যা Next.js-এর docs-এ এখন "Previous Model" নামে আলাদা রাখা।

"Default" কথাটি তাই সবসময় একটি config-এর সাথে বলতে হয়। এই পর্বের সব run দুটির একটি থেকে: (ক) create-next-app@16.4.4 --yes-এর template, যার next.config.ts-এ লেখা ছিল cacheComponents: true আর partialPrefetching: true; অথবা (খ) একই প্রকল্প, next.config.ts খালি করে, অর্থাৎ cacheComponents বন্ধ। যেখানে আলাদা করে বলা নেই, সেখানে (ক)। প্রকল্পের config এটি:

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
};

এখন নিয়ম সহজ। কোনো async ফাংশন বা component-এর শুরুতে "use cache" লিখলে তার ফল জমা থাকে। Argument-গুলো cache key-এর অংশ, তাই getProduct(1) আর getProduct(2) আলাদা জমা। কতক্ষণ থাকবে তা বলে cacheLife, আর কোন দলের তা বলে cacheTag:

import { cacheLife, cacheTag } from "next/cache";

// সবার জন্য একই তালিকা: cache-এ রাখা, ট্যাগ 'products'
export async function getProducts() {
  "use cache";
  cacheLife("hours");
  cacheTag("products");
  return db.listProducts();
}

export async function getProduct(id: number) {
  "use cache";
  cacheLife("hours");
  cacheTag("products", `product-${id}`);
  return db.getProduct(id);
}

যা cache করা নেই, যেমন আজকের স্টক, তা প্রতিটি অনুরোধে চলে। Next.js চায় আপনি এমন অংশকে Suspense-এ রাখুন। connection() ডাকলে Next.js বোঝে "এই অংশ build-এ বানানো যাবে না, অনুরোধ এলে চালাতে হবে":

export default async function ProductsPage() {
  const products = await getProducts();          // cache থেকে
  return (
    <main>
      <h1>বাজারঘর — সব পণ্য</h1>
      <ul>{products.map((p) => <li key={p.id}>…{p.name} — ৳{p.price}</li>)}</ul>
      <Suspense fallback={<p>আজকের স্টক আসছে…</p>}>
        <TodayStock />
      </Suspense>
    </main>
  );
}
async function TodayStock() {
  await connection();                            // প্রতিটি অনুরোধে নতুন করে
  const n = await db.getStock(1);
  return <p>সরিষার তেল স্টকে আছে: {n}টি</p>;
}

Build করলে Next.js প্রতিটি route-এর একটি চিহ্ন দেয়। Path ছোট করে ~/bazarghar লেখা হয়েছে।

সত্যিকারের run · ~/bazarghar$ npx next build · Next.js 16.4.4 (Turbopack), Node.js 24

▲ Next.js 16.4.4 (Turbopack)
- Cache Components enabled
- Partial Prefetching enabled
✓ Compiled successfully in 496ms
…
[db #1] SELECT product 1 206ms
[db #2] SELECT products 310ms
✓ Generating static pages using 16 workers (15/15) in 1255ms

Route (app)           Revalidate  Expire
┌ ○ /
├ ◐ /account
├ ƒ /api/price
├ ƒ /blocking
├ ◐ /products                 1h      1d
├   /products/[id]
│ ├ ◐ /products/[id]
│ └ ◐ /products/1             1h      1d
…
○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content
ƒ  (Dynamic)            server-rendered on demand

পড়ার কৌশল: ◐ মানে পাতার একটি static shell build-এর সময় তৈরি হয়ে আছে, আর Suspense-এর ভেতরের অংশ অনুরোধের সময় stream হবে। একে Next.js বলে Partial Prerendering; বিস্তারিত পর্ব ৫৫-তে (SSR, SSG, ISR আর partial prerendering). /products-এর পাশে 1h আর 1d এসেছে cacheLife("hours") থেকে। আর build-এর সময়েই ডেটাবেসে দুটি query গেছে: তালিকা আর পণ্য ১-এর shell বানাতে। এরপর server চালু করে /products খুললে log-এ SELECT products আসে না, শুধু স্টকের query আসে।

cacheComponents বন্ধ থাকলে কী হয়

একই প্রকল্পে config খালি করে build দেওয়া হলো (config খ)। প্রথমে build-ই থামল, কারণ "use cache" আর instant এই flag ছাড়া চলে না:

সত্যিকারের run · config খ (cacheComponents বন্ধ) · Next.js 16.4.4 · build ব্যর্থ, অংশবিশেষ

./app/blocking/page.tsx:5:14
Error: Route segment config "instant" requires `nextConfig.cacheComponents` to be enabled.
./lib/data.ts:7:3
Error: To use "use cache", please enable the feature flag `cacheComponents` in your Next.js config.

তারপর "use cache"-এর লাইনগুলো সরিয়ে, /blocking বাদ দিয়ে, আর একটি নতুন পাতা /plain যোগ করে আবার build. /plain শুধু await getProducts() করে, কোনো connection() বা cookies() ছাড়া।

সত্যিকারের run · config খ · Next.js 16.4.4 · route table-এর অংশ আর তারপর next start-এ অনুরোধ · একটি local কম্পিউটারে একবার

[db #3] SELECT products 303ms        ← build-এর সময়
├ ○ /plain
├ ƒ /products
├ ƒ /products/[id]
├ ƒ /account
…
GET /plain → ৳320   GET /products → ৳320
POST /api/price id=1 340 → {"ok":true,"id":1,"price":340}
GET /plain → ৳320   GET /products → ৳340
GET /plain → ৳320   GET /products → ৳340
/plain-এর header: x-nextjs-cache: HIT · Cache-Control: s-maxage=31536000
একই কোডক: template (cacheComponents: true)খ: cacheComponents বন্ধ
"use cache", instantচলেbuild error
Suspense-এর ভেতরে connection() সহ পাতা◐: shell আগে, বাকিটা streamƒ: পুরো পাতা প্রতিটি অনুরোধে
Suspense ছাড়া cookies()build error, সিদ্ধান্ত চায়চুপচাপ ƒ
কিছু না বলে ডেটাবেস পড়া পাতা (/plain)build error, cache বা Suspense চায় (অংশ ৬)চুপচাপ ○: build-এর দাম ধরে রাখে, দাম বদলের পরও ৳৩২০

শেষ সারিটিই আগের মডেলের পরিচিত ফাঁদ। /plain কোনো cache-এর কথা বলেনি, তবু build-এর সময়ের দাম জমে গেল, আর revalidateTag('products', 'max')-ও তাকে ছুঁতে পারল না, কারণ তার গায়ে কোনো ট্যাগ নেই। এই config-এ তাকে নতুন করতে revalidatePath('/plain') বা নতুন deploy লাগত। Cache Components এই নীরব সিদ্ধান্তটিকেই build error বানিয়ে আপনার সামনে আনে।

Profilestalerevalidateexpire
default৫ মিনিট১৫ মিনিটকখনো না
seconds৩০ সেকেন্ড১ সেকেন্ড৬০ সেকেন্ড
minutes৫ মিনিট১ মিনিট১ ঘণ্টা
hours৫ মিনিট১ ঘণ্টা১ দিন
days৫ মিনিট১ দিন১ সপ্তাহ
max৫ মিনিট৩০ দিন১ বছর

তিনটি সংখ্যার মানে: stale হলো browser কতক্ষণ প্রশ্ন না করে নিজের কপি ব্যবহার করবে। revalidate পার হলে server পরের অনুরোধে পুরোনো কপি দিয়েই পেছনে নতুন বানায়। expire পার হলে পুরোনো কপি আর দেওয়া হয় না, অনুরোধকে নতুন ডেটার জন্য অপেক্ষা করতে হয়। Docs অনুযায়ী seconds profile বা ৫ মিনিটের কম expire-এর cache "short-lived" ধরা হয়, আর সেটি static shell-এ ঢোকে না।

একটি অনুরোধ কোন কোন স্তর পার হয়

Each layer answers if it can; only a miss everywhere reaches the database

Next.js 16-এ data cache-এর পাঁচটি স্তর Browser · prefetch Static shell "use cache" store React cache() ডেটাবেস একজনের · stale সময় পর্যন্ত build বা ISR-এ বানানো HTML server-এ · cacheLife, cacheTag শুধু একটি অনুরোধের ভেতরে সত্যের উৎস · সবচেয়ে ধীর
ছবি ৫৩.২ — Redis-এর মতো বাইরের cache (পর্ব ৪৪) এর পাশে আলাদা আরেকটি স্তর হিসেবে বসতে পারে।

বাজারঘরের দুই নম্বর পণ্য build-এ বানানো হয়নি, কারণ generateStaticParams শুধু 1 ফেরায়। তাই প্রথম ক্রেতা একটি cache miss দেখবেন, আর দ্বিতীয়জন hit. নিচে আসল run ধাপে ধাপে দেখুন:

  1. BrowserGET /products/2প্রথম ক্রেতা
  2. App shellloading.tsxসঙ্গে সঙ্গে
  3. missSELECT product 2ডেটাবেসে
  4. রিভিউSELECT reviews 2প্রতিবার
  5. hitদ্বিতীয় অনুরোধপণ্যের query নেই
অনুরোধid=2, build-এ বানানো হয়নি
GET /products/2
"use cache" store-এ getProduct(2) নেই
প্রথম HTML টুকরো+46 ms
<template> <p>পণ্যের পাতা আসছে…</p>
params অজানা ছিল, তাই loading.tsx-এর fallback
server logcache miss
[db #4] SELECT product 2 206ms
+257 ms: <h1>মিনিকেট চাল ৫ কেজি … <p>রিভিউ লোড হচ্ছে…
server logcache করা নেই
[db #5] SELECT reviews 2 1214ms
+1463 ms: <li>চাল ঝরঝরে
পুরো পাতা: 1467 ms
দ্বিতীয় অনুরোধhit
+31 ms: <h1>মিনিকেট চাল ৫ কেজি … দাম
[db #6] SELECT reviews 2 1205ms   ← শুধু এটি
পুরো পাতা: 1252 ms
  1. অনুরোধ। প্রথম ক্রেতা পণ্য ২ খুললেন। getProduct(2)-এর কোনো জমা ফল নেই, কারণ build শুধু পণ্য ১ বানিয়েছিল।
  2. App shell আগে। ৪৬ ms-এ প্রথম HTML টুকরো এলো, ভেতরে loading.tsx-এর লেখা "পণ্যের পাতা আসছে…"। ক্রেতা ফাঁকা পর্দা দেখছেন না।
  3. Cache miss. এবার ডেটাবেসে আসল query: [db #4] SELECT product 2 206ms. ফলটি "use cache" store-এ জমা হলো। ২৫৭ ms-এ নাম আর দাম পৌঁছাল, সঙ্গে রিভিউর fallback.
  4. রিভিউ। রিভিউ cache করা নয়, তাই ১,২১৪ ms-এর query চলল। ১,৪৬৩ ms-এ রিভিউ এলো, পুরো পাতা শেষ ১,৪৬৭ ms-এ।
  5. Cache hit. দ্বিতীয় অনুরোধে পণ্যের query একেবারেই নেই। Next.js পাতাটির shell-ও পেছনে বানিয়ে রেখেছে, তাই ৩১ ms-এর প্রথম টুকরোতেই নাম আর দাম। বাকি শুধু রিভিউর ১,২০৫ ms.

আরেকটি জিনিস এই run-এ দেখা গেল। /blocking নামের একটি পাতা অনুরোধের সময় getProduct(1) ডাকল, আর server চালুর পর প্রথমবার ডেটাবেসে SELECT product 1 গেল, যদিও build-এর সময় এই ফল একবার বানানো হয়েছিল। Build-এর ফল static shell-এর HTML-এ ঢুকেছিল, কিন্তু server-এর memory-র store খালি অবস্থায় শুরু হয়েছিল। Docs-ও বলে, default store প্রতিটি server instance-এর নিজের memory-তে থাকে, আর serverless পরিবেশে তা টিকে না-ও থাকতে পারে। সবার মধ্যে ভাগ করা টেকসই cache লাগলে আছে "use cache: remote"; সেটি একটি নেটওয়ার্ক hop যোগ করে, তাই hit rate বেশি হলেই লাভ।

কত সাশ্রয়: একটি হিসাব

ধরা যাক (অনুমিত সংখ্যা) বাজারঘরের পণ্য-তালিকা ঘণ্টায় ১০,০০০ বার খোলা হয়, আর তালিকার query ৩০০ ms. Cache ছাড়া ডেটাবেস ঘণ্টায় ১০,০০০ × ০.৩ = ৩,০০০ সেকেন্ড কাজ করে, অর্থাৎ গড়ে প্রায় ০.৮৩টি query সবসময় চলছে। cacheLife("hours") দিলে প্রতিটি server instance ঘণ্টায় মোটামুটি একবার নতুন বানায়। চারটি instance থাকলে ঘণ্টায় প্রায় ৪টি query, মোট ১.২ সেকেন্ড। Hit rate দাঁড়ায় (১০,০০০ − ৪) ÷ ১০,০০০ ≈ ৯৯.৯৬%. দাম হলো সতেজতা: দাম বদলের পর এক ঘণ্টা পর্যন্ত পুরোনো তালিকা দেখা যেতে পারে। সেই ফাঁক বন্ধ করে পরের অংশের ট্যাগ।

৫ · দাম বদলালে: cacheTag, revalidateTag, updateTag

সময় ধরে নতুন করা (cacheLife) অনুমানের ওপর চলে। কিন্তু দোকানি দাম বদলালে আপনি ঠিক জানেন কোন ডেটা পুরোনো হলো। তখন ট্যাগ ধরে বলা যায়: "products ট্যাগের সব জমা পুরোনো।" এটি on-demand revalidation.

মুদি দোকানের দেয়ালে দামের তালিকা টাঙানো। সরিষার তেলের দাম বাড়ল। দোকানি দুইভাবে কাজটি করতে পারেন। এক, পুরোনো কাগজটি রেখেই নতুন তালিকা লিখতে বসেন; মাঝে যে খদ্দের আসেন তিনি পুরোনো দাম দেখেন, পরের জন নতুন। দুই, কাগজটি তখনই ছিঁড়ে ফেলেন; পরের খদ্দেরকে একটু দাঁড়াতে হয়, কিন্তু তিনি নিশ্চিতভাবে নতুন দাম দেখেন। প্রথমটি revalidateTag(tag, 'max'), দ্বিতীয়টি updateTag(tag).

APIকোথায় চলেআচরণকখন
revalidateTag(tag, 'max')Server Action, Route handlerপুরোনোটা একবার দেয়, পেছনে নতুন বানায় (stale-while-revalidate)ক্যাটালগ, ব্লগ, webhook থেকে আসা বদল
revalidateTag(tag, { expire: 0 })Server Action, Route handlerসঙ্গে সঙ্গে বাতিলRoute handler-এ যখন পুরোনো দেখানো চলবে না
updateTag(tag)শুধু Server Actionসঙ্গে সঙ্গে বাতিল; যিনি বদলালেন তিনি নিজের বদল দেখেননিজের প্রোফাইল, নিজের দেওয়া রিভিউ
revalidatePath(path)Server Action, Route handlerএকটি path-এর সব জমা ডেটাকোন ট্যাগ জানা নেই এমন ক্ষেত্রে; docs ট্যাগকেই আগে রাখতে বলে

একটি argument দিয়ে revalidateTag('products') লেখা পুরোনো ধরন, Next.js 16-এর docs-এ deprecated. এটি এখন { expire: 0 }-এর মতো আচরণ করে, আর ভবিষ্যতে সরে যেতে পারে। AI প্রায়ই এক-argument-এর রূপটিই লেখে।

বাজারঘরে দাম বদলের জন্য একটি Route handler আছে। এতে নকল একটি admin key যাচাই হয় (আসল প্রকল্পে session আর role, পর্ব ৪৮), ইনপুট যাচাই হয় (পর্ব ২৭-এর মতো), তারপর ডেটাবেস বদলে ট্যাগ পুরোনো চিহ্নিত হয়:

import { revalidateTag } from "next/cache";
import { db } from "@/lib/db";

export async function POST(req: Request) {
  if (req.headers.get("x-admin-key") !== process.env.ADMIN_KEY) {
    return Response.json({ error: "unauthorized" }, { status: 401 });
  }
  const { id, price } = await req.json();
  if (!Number.isInteger(id) || !Number.isInteger(price) || price <= 0) {
    return Response.json({ error: "bad input" }, { status: 400 });
  }
  if (!(await db.setPrice(id, price))) return Response.json({ error: "not found" }, { status: 404 });
  revalidateTag("products", "max");
  return Response.json({ ok: true, id, price });
}

সত্যিকারের run · Next.js 16.4.4, next start, ADMIN_KEY=example-secret · একটি local কম্পিউটারে একবার · প্রতিটি লাইন একটি অনুরোধ

GET  /products      → ৳320 ৳410 ৳150
GET  /products/1    → ৳320
POST /api/price (key ছাড়া)          → {"error":"unauthorized"}  http=401
POST /api/price (key সহ) id=1 340    → {"ok":true,"id":1,"price":340}  http=200
GET  /products      → ৳320 ৳410 ৳150     ← পুরোনো, পেছনে নতুন তৈরি
GET  /products      → ৳340 ৳410 ৳150
GET  /products/1    → ৳320               ← পুরোনো
GET  /products/1    → ৳340

server log:
[db] UPDATE products SET price=340 WHERE id=1
[db #26] SELECT products 314ms
[db #29] SELECT product 1 215ms

দাম বদলের পর প্রথম অনুরোধ পুরোনো ৩২০ দেখল, আর ঠিক তখনই পেছনে SELECT products চলল। পরের অনুরোধ ৩৪০. পণ্যের পাতাও নতুন হলো, কারণ getProduct-এর গায়েও products ট্যাগ ছিল। একটি ট্যাগ অনেক জমাকে একসাথে ধরতে পারে, আর একটি জমায় একাধিক ট্যাগ থাকতে পারে। নিচে ঘটনাটি ধাপে ধাপে দেখুন।

1POST /api/price   (key নেই)2POST /api/price   {id:1, price:340}3  db.setPrice(1, 340)4  revalidateTag("products", "max")5GET /products     ক্রেতা ক6GET /products     ক্রেতা খ
এই মুহূর্তের অবস্থা
ডেটাবেসে দাম
cache-এ দাম
cache-এর অবস্থা
ক্রেতা দেখলেন
  1. Key ছাড়া অনুরোধ। x-admin-key নেই, তাই ৪০১. ডেটাবেস আর cache দুটোই ৩২০-তে অপরিবর্তিত।
  2. বৈধ অনুরোধ। Key মিলেছে, id আর price পূর্ণসংখ্যা, দাম শূন্যের বেশি। এখনো কিছু বদলায়নি।
  3. ডেটাবেস বদলাল। সত্যের উৎসে দাম ৩৪০. কিন্তু cache জানে না, এখনো ৩২০ রাখে আর নিজেকে fresh ভাবে।
  4. ট্যাগ পুরোনো চিহ্নিত। revalidateTag("products", "max") জমাটি মুছে ফেলেনি, শুধু stale চিহ্ন দিয়েছে। উত্তর ২০০.
  5. ক্রেতা ক। তিনি সঙ্গে সঙ্গে পুরোনো ৳৩২০ পেলেন, অপেক্ষা ছাড়াই। পেছনে SELECT products চলে cache-এ ৩৪০ বসাল।
  6. ক্রেতা খ। এবার cache fresh আর ৩৪০. ডেটাবেসে কোনো query যায়নি। Stale-while-revalidate-এর দাম ছিল ঠিক একজন ক্রেতার পুরোনো দাম দেখা।

নিচের মডেলে একই ঘটনা, সঙ্গে updateTag-এর তুলনা। এটি শেখানোর মডেল, Next.js-এর ভেতরের কোড নয়।

// শেখানোর মডেল: Next.js-এর tag-cache-এর সরল রূপ, আসল কোড নয়
const db = { price: 320 };
let dbHits = 0;
const store = new Map();                        // key -> { value, tags, stale }
function cached(key, tags, load) {
  const e = store.get(key);
  if (e && !e.stale) return { v: e.value, how: 'HIT' };
  if (e) {                                      // পুরোনোটা এখনই, নতুনটা পেছনে
    queueMicrotask(() => { dbHits++; store.set(key, { value: load(), tags, stale: false }); });
    return { v: e.value, how: 'STALE' };
  }
  dbHits++;
  const value = load();
  store.set(key, { value, tags, stale: false });
  return { v: value, how: 'MISS' };
}
const revalidateTag = (tag) => {                // 'max': শুধু stale চিহ্ন
  for (const e of store.values()) if (e.tags.includes(tag)) e.stale = true;
};
const updateTag = (tag) => {                    // সঙ্গে সঙ্গে বাতিল
  for (const [k, e] of store) if (e.tags.includes(tag)) store.delete(k);
};
const getProducts = () => cached('getProducts', ['products'], () => db.price);
async function visit(label) {
  const r = getProducts();
  console.log(`${label}  ${r.how.padEnd(5)} price=${r.v}  dbHits=${dbHits}`);
  await null;                                   // পেছনের কাজ শেষ হতে দিন
}
(async () => {
  await visit('visit 1'); await visit('visit 2');
  db.price = 340; revalidateTag('products'); console.log('-- price 340, revalidateTag');
  await visit('visit 3'); await visit('visit 4');
  db.price = 360; updateTag('products'); console.log('-- price 360, updateTag');
  await visit('visit 5'); await visit('visit 6');
})();

visit 1 MISS price=320 dbHits=1 তারপর visit 2 HIT. revalidateTag-এর পর visit 3 STALE price=320 dbHits=1: পুরোনো দাম গেল, আর ছাপার পরে পেছনের কাজ একটি query করল, তাই visit 4 HIT price=340 dbHits=2. updateTag-এর পর visit 5 MISS price=360 dbHits=3: পুরোনো জমা নেই, তাই সে নতুনের জন্য অপেক্ষা করল কিন্তু সঠিক দাম পেল। এই পার্থক্যটাই মূল কথা: একটিতে গতি আগে, অন্যটিতে নিজের বদল সঙ্গে সঙ্গে দেখা আগে।

updateTag বাস্তবে কেমন দেখায়, তা form জমা দেওয়ার Server Action-এ: দোকানি দাম বদলে "সেভ" চাপলেন, তারপর নিজের পাতায় ফিরে গেলেন। তিনি পুরোনো দাম দেখলে ভাববেন সেভ হয়নি। এই কারণেই এখানে updateTag. Server Action কীভাবে লিখতে হয় আর তাকে কীভাবে সুরক্ষিত রাখতে হয়, তা পর্ব ৫৪-তে (Server Action).

৬ · অনুরোধ-নির্ভর API: cookies(), headers(), searchParams

কিছু তথ্য build-এর সময় জানা সম্ভবই নয়: কে লগইন করেছেন (cookie), কোন ভাষা চান (header), কোন ফিল্টার বেছেছেন (URL-এর ? অংশ)। এগুলো পড়ার API-কে Next.js-এর docs বলে runtime API বা request-time API: runtime API. Next.js 15 থেকে এগুলো async: await cookies(), await headers(), আর page-এর params ও searchParams একটি Promise.

আগের মডেলে একটি cookies() ডাকলেই পুরো route dynamic হয়ে যেত। Cache Components-এ নিয়ম আরও সূক্ষ্ম: যে component এগুলো পড়ে, শুধু সেটিকে Suspense-এ রাখতে হয়। না রাখলে কী হয়, দেখুন:

import { cookies } from "next/headers";

export default async function AccountBad() {
  const name = (await cookies()).get("name")?.value ?? "অতিথি";
  return <main><h1>আমার অ্যাকাউন্ট</h1><p>স্বাগতম, {name}</p></main>;
}

সত্যিকারের run · ~/bazarghar$ npx next build · Next.js 16.4.4 · build ব্যর্থ

Error: Route "/account-bad": Next.js encountered uncached or runtime data during prerendering.

`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.

Ways to fix this:
  - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
  - [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`)
  - [block] Set `export const instant = false` to allow a blocking route
…
Export encountered an error on /account-bad/page: /account-bad, exiting the build.

Error নিজেই তিনটি পথ বলে দিচ্ছে। সবচেয়ে ভালো প্রথমটি: অনুরোধ-নির্ভর অংশটুকু আলাদা component-এ নিয়ে Suspense-এ রাখা। তাহলে শিরোনামসহ বাকি পাতা static shell-এ থাকে:

export default function AccountPage() {
  return (
    <main>
      <h1>আমার অ্যাকাউন্ট</h1>
      <Suspense fallback={<p>…</p>}>
        <Greeting />
      </Suspense>
    </main>
  );
}
async function Greeting() {
  const name = (await cookies()).get("name")?.value ?? "অতিথি";
  return <p>স্বাগতম, {name}</p>;
}

এবার build সফল, আর route table-এ ◐ /account. Cookie পাঠিয়ে আর না পাঠিয়ে দুবার খোলা হলো; HTML-এ আসল লেখা ছিল স্বাগতম, Rahima আর স্বাগতম, অতিথি. তৃতীয় পথ instant = false জেনেশুনে পুরো পাতাকে অপেক্ষা করতে দেয়; build-এ সেই route পায় ƒ চিহ্ন। এর খরচ অংশ ৭-এ মাপা হয়েছে।

cookies() পড়া কোনো ফাংশনকে "use cache" দিয়ে মুড়বেন না। একজনের নাম বা কার্ট তখন সবার কাছে চলে যেতে পারত। Next.js নিজেও "use cache"-এর ভেতরে runtime API পড়তে দেয় না। বদলে, cookie থেকে মান বের করে argument হিসেবে cached ফাংশনে পাঠান, যাতে মানটি cache key-এর অংশ হয়। ব্যক্তিগত ডেটার জন্য docs-এ আলাদা "use cache: private" আছে, যা ফল শুধু সেই browser-এ রাখে।

৭ · Streaming: loading.js আর <Suspense>

পণ্যের পাতায় রিভিউ সবচেয়ে ধীর, ১,২০০ ms. সবকিছু রিভিউর জন্য আটকে রাখলে ক্রেতা ১.২ সেকেন্ড সাদা পর্দা দেখবেন। Streaming-এ server দ্রুত অংশটুকু আগে পাঠায়, ধীর অংশের জায়গায় একটি fallback রাখে, আর পরে একই HTTP উত্তরে বাকিটা পাঠায়। উত্তরটি একটিই, শুধু কয়েক টুকরোয় আসে। HTTP/1.1-এ এটি হয় chunked transfer encoding দিয়ে; আমাদের প্রতিটি উত্তরে header ছিল transfer-encoding: chunked.

দাওয়াতের বাড়িতে সব রান্না শেষ হওয়া পর্যন্ত অতিথিকে বসিয়ে রাখা হয় না। আগে আসে শরবত আর সালাদ, টেবিলে রোস্টের প্লেটের জায়গা খালি থাকে। রোস্ট তৈরি হলে ঠিক সেই জায়গায় বসে যায়। Fallback হলো খালি প্লেট, আর streamed অংশ হলো রোস্ট।

Next.js-এ দুইভাবে Suspense বসানো যায়। loading.js (বা loading.tsx) ফাইল রাখলে Next.js পুরো page-কে নিজে থেকে একটি Suspense-এ মুড়ে দেয়। আর <Suspense> নিজে বসালে আপনি ঠিক করেন কোন ছোট অংশ অপেক্ষা করবে। বাজারঘরের পণ্যের পাতায় দুটিই আছে:

export async function generateStaticParams() {
  return [{ id: "1" }];
}
export default async function ProductPage({ params }: PageProps<"/products/[id]">) {
  const { id } = await params;
  const product = await getProduct(Number(id));     // "use cache"
  if (!product) notFound();
  return (
    <main>
      <h1>{product.name}</h1>
      <p>দাম: ৳{product.price}</p>
      <Suspense fallback={<p>রিভিউ লোড হচ্ছে…</p>}>
        <Reviews id={product.id} />
      </Suspense>
    </main>
  );
}
async function Reviews({ id }: { id: number }) {
  await connection();
  const reviews = await db.getReviews(id);          // ১২০০ ms, cache নেই
  return <ul>{reviews.map((r) => <li key={r}>{r}</li>)}</ul>;
}

এবার curl দিয়ে মাপা হলো। time_starttransfer হলো প্রথম byte আসার সময় (TTFB), time_total পুরো উত্তর।

সত্যিকারের run · curl 8.18, Next.js 16.4.4 next start · একটি local কম্পিউটারে একবার

$ curl -s -o p.html -w "%{http_code} ttfb=%{time_starttransfer}s total=%{time_total}s %{size_download}B\n" http://127.0.0.1:3753/products
200 ttfb=0.033535s total=0.474951s 14598B
$ curl … http://127.0.0.1:3753/products/1
200 ttfb=0.017985s total=1.251362s 16169B
$ curl … http://127.0.0.1:3753/blocking
200 ttfb=0.008724s total=1.425964s 11730B

শেষ লাইনটি অবাক করার মতো। /blocking পাতা Suspense ছাড়া রিভিউর জন্য অপেক্ষা করে, তবু TTFB মাত্র ৯ ms! কারণ Next.js status আর header আগেই পাঠিয়ে দেয়। তাই TTFB একা বিভ্রান্ত করতে পারে। আসল প্রশ্ন হলো, দেখার মতো HTML কখন এলো? সেটি মাপতে একটি ছোট Node script প্রতিটি টুকরো কখন এলো আর তাতে কী লেখা ছিল তা ছাপল:

সত্যিকারের run · Node.js 24-এর fetch দিয়ে প্রতিটি টুকরোর সময় · একটি local কম্পিউটারে একবার · লেখা ছোট করা

/products/1   (Suspense দিয়ে stream)
chunk 1  +41ms    1684B  <h1>সরিষার তেল ১ লিটার <p>দাম: ৳ … <template>
chunk 2  +45ms    6738B
chunk 3  +47ms    6367B
chunk 4  +1255ms   306B
chunk 6  +1256ms   987B  <li>খাঁটি ঝাঁঝ <li>সময়মতো পেয়েছি
done  1258ms

/blocking     (instant = false, Suspense নেই)
chunk 1  +1243ms  1371B  <h1>সরিষার তেল ১ লিটার <li>খাঁটি ঝাঁঝ <li>সময়মতো পেয়েছি
chunk 2  +1243ms 10345B
done  1246ms
পাতা (একটি local run)নাম আর দাম দেখা গেলধীর অংশ এলোপুরো পাতা
পণ্য-তালিকা /products (তালিকা cache, স্টক stream)৫০ msস্টক ৪৫৮ ms৪৬১ ms
পণ্য ১ (shell build-এ তৈরি)৪১ msরিভিউ ১,২৫৬ ms১,২৫৮ ms
পণ্য ২, প্রথম ক্রেতা (cache miss)২৫৭ ms (৪৬ ms-এ loading)রিভিউ ১,৪৬৩ ms১,৪৬৭ ms
পণ্য ২, দ্বিতীয় ক্রেতা (hit)৩১ msরিভিউ ১,২৫০ ms১,২৫২ ms
/blocking (Suspense নেই)১,২৪৩ msএকসাথে১,২৪৬ ms

পুরো পাতার সময় প্রায় একই, কারণ ১,২০০ ms-এর রিভিউ এড়ানো যায়নি। কিন্তু ক্রেতা নাম আর দাম দেখলেন ৪১ ms-এ, ১,২৪৩ ms-এ নয়, অর্থাৎ প্রায় ৩০ গুণ আগে। Streaming মোট কাজ কমায় না, অপেক্ষার অনুভূতি আর প্রথম দেখার সময় কমায়।

Browser-এ টুকরোগুলো জোড়া লাগে কীভাবে? প্রথম টুকরোতে fallback-এর জায়গায় একটি চিহ্ন থাকে। পরের টুকরোতে আসল রিভিউ একটি লুকানো div-এ আসে, সঙ্গে একটি ছোট script, যা দুটি জায়গা বদলে দেয়। নিচে আসল HTML থেকে নেওয়া অংশগুলো ধাপে ধাপে:

  1. অনুরোধGET /products/1০ ms
  2. chunk 1+41 msshell + fallback
  3. chunk 2–3+47 msJS আর RSC payload
  4. chunk 4–6+1256 msরিভিউ
  5. শেষ1258 msজায়গা বদল
browserঅনুরোধ গেল
GET /products/1 HTTP/1.1
server: cached shell আছে, রিভিউর query শুরু
[db] SELECT reviews 1 …
chunk 1+41 ms · 1684 B
<h1>সরিষার তেল ১ লিটার</h1><p>দাম: ৳<!-- -->320</p>
<!--$?--><template id="B:0"></template>
<p>রিভিউ লোড হচ্ছে…</p><!--/$-->
chunk 2–3+45, +47 ms
<script src="/_next/static/chunks/…">
self.__next_f.push([1, "…"])
← RSC payload: component গাছের তথ্য
chunk 4–6+1255 ms
<div hidden id="S:0"><ul><li>খাঁটি ঝাঁঝ</li>
<li>সময়মতো পেয়েছি</li></ul></div>
scriptজায়গা বদল
$RC("B:0","S:0")
B:0-এর fallback সরে, S:0-এর রিভিউ বসে
done 1258ms · মোট 16169 B
  1. অনুরোধ। Browser পণ্য ১ চাইল। Server-এর কাছে build-এ বানানো shell তৈরি আছে। সে সঙ্গে সঙ্গে রিভিউর ধীর query শুরু করল।
  2. প্রথম টুকরো, ৪১ ms. নাম আর দাম সহ HTML এলো। রিভিউর জায়গায় fallback "রিভিউ লোড হচ্ছে…", আর তার আগে template id="B:0" নামের একটি চিহ্ন। ক্রেতা এখনই পড়তে পারছেন।
  3. Script আর RSC payload, ৪৫–৪৭ ms. React-এর JavaScript আর component গাছের তথ্য এলো, যাতে browser পাতাটিকে interactive করতে পারে। এই ধাপের নাম hydration, পর্ব ৫৬-তে বিস্তারিত।
  4. রিভিউ, ১,২৫৫ ms. Query শেষ। একই উত্তরের ভেতরে নতুন টুকরো এলো: একটি লুকানো div id="S:0", ভেতরে দুটি রিভিউ।
  5. জায়গা বদল। ছোট script $RC("B:0","S:0") fallback সরিয়ে রিভিউ বসাল। উত্তর শেষ ১,২৫৮ ms-এ, মোট ১৬,১৬৯ byte. পুরো সময়ে সংযোগ একটিই ছিল।

নিচের live React ব্লকে একই ধারণা browser-এর ভেতরে দেখুন। এটি শেখানোর মডেল: এখানে server নেই, একটি Promise ৮০০ ms পরে রিভিউ দেয়, আর React-এর use() সেই Promise-এর জন্য Suspense-কে অপেক্ষা করায়।

<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 reviewsP = new Promise((ok) =>
  setTimeout(() => ok(['খাঁটি ঝাঁঝ', 'সময়মতো পেয়েছি']), 800));

function Reviews() {
  const list = use(reviewsP);          // তৈরি না হলে Suspense অপেক্ষা করে
  console.log(`reviews shown ~${at()} ms`);
  return <ul>{list.map((r) => <li key={r}>{r}</li>)}</ul>;
}
function ProductPage() {
  useEffect(() => console.log(`shell shown ~${at()} ms`), []);
  return (
    <main>
      <h1>সরিষার তেল ১ লিটার</h1>
      <p>দাম: ৳320</p>
      <Suspense fallback={<p>রিভিউ লোড হচ্ছে…</p>}>
        <Reviews />
      </Suspense>
    </main>
  );
}
createRoot(document.getElementById('root')).render(<ProductPage />);
</script>

চালানোর সাথে সাথে preview-তে নাম, দাম আর "রিভিউ লোড হচ্ছে…" দেখা যায়, আর নিচে আসে shell shown ~0 ms. প্রায় ৮০০ ms পরে fallback-এর জায়গায় দুটি রিভিউ বসে, আর দ্বিতীয় লাইন আসে reviews shown ~800 ms (Edge-এ চালিয়ে ২ সেকেন্ড পরে পড়া আসল আউটপুট)। Reviews component প্রথমবার Promise তৈরি না পেয়ে "থেমে" যায়, Suspense তখন fallback দেখায়, আর Promise পূর্ণ হলে React শুধু সেই অংশটুকু আবার render করে। Server-এর streaming-এ একই সীমারেখা কাজ করে; তফাত হলো সেখানে বাকি অংশ একই HTTP উত্তরের পরের টুকরোয় আসে। 800 বদলে 3000 করে আবার চালিয়ে দেখুন।

৮ · কিছু ভাঙলে: error.js

ডেটাবেস ধীর হতে পারে, আবার ভেঙেও যেতে পারে। পরীক্ষার জন্য পণ্য ৩-এর রিভিউ আনার পর ইচ্ছা করে error ছোড়া হলো: throw new Error("reviews table timeout"). Route-এর ফোল্ডারে একটি error.js ফাইল রাখলে Next.js সেই অংশকে একটি error boundary-তে মুড়ে দেয়। এটি অবশ্যই Client Component, কারণ "আবার চেষ্টা" বোতাম browser-এ চলে।

"use client";
import { useEffect } from "react";

export default function Error({ error, retry }: { error: unknown; retry: () => void }) {
  useEffect(() => { console.error(error); }, [error]);
  const digest = error instanceof Error ? (error as Error & { digest?: string }).digest : undefined;
  return (
    <div role="alert">
      <p>দুঃখিত, এই অংশটি এখন আনা গেল না।</p>
      {digest && <small>রেফারেন্স: {digest}</small>}
      <button onClick={() => retry()}>আবার চেষ্টা করুন</button>
    </div>
  );
}

অনেক টিউটোরিয়ালে prop-এর নাম reset আর error: Error. লেখার সময়ের docs-এ মূল prop retry, যা segment-টি আবার fetch করে render করে; Next.js 16.2-তে unstable_retry নামে এসে 16.3-এ stable হয়েছে। reset এখনো আছে, কিন্তু সেটি আবার fetch করে না। আর error-এর type এখন unknown, কারণ Client Component যেকোনো মান ছুড়তে পারে।

সত্যিকারের run · Next.js 16.4.4 next start · GET /products/3 · server log (path ছোট করা) আর browser-এ যাওয়া HTML-এর অংশ

[db #5] SELECT product 3 213ms
[db #6] SELECT reviews 3 1210ms
⨯ Error: reviews table timeout
    at k (~/bazarghar/.next/server/chunks/ssr/…js:1:1106) {
  digest: '1751822584'
}

status 200   done 1464ms
… $RX("B:2","1751822584") …
… self.__next_f.push([1,"27:E{\"digest\":\"1751822584\"}\n"]) …

তিনটি জিনিস শেখার আছে। এক: browser পেয়েছে শুধু digest 1751822584; "reviews table timeout" লেখাটি HTML-এ একবারও নেই (grep -c timeout দিল 0). Production-এ Server Component-এর error-এর আসল বার্তা লুকানো থাকে, যাতে টেবিলের নাম বা গোপন তথ্য বাইরে না যায়। Log-এ digest দিয়ে খুঁজে আসল কারণ পাওয়া যায়; পর্ব ২৮-এ (Error handling) Express-এ একই নীতি দেখেছেন। দুই: status 200. Stream শুরু হলে header আগেই চলে গেছে, পরে আর ৫০০ পাঠানো যায় না। তাই monitoring শুধু status code দেখে চললে এই error ধরা পড়বে না; server log বা error-reporting-ও দেখতে হবে। তিন: ভাঙন শুধু সেই segment-এ আটকে থাকে, nav bar আর বাকি layout ঠিকঠাক থাকে।

পণ্য না পেলে আলাদা পথ: notFound() ডাকলে not-found.js দেখায়। "পণ্য নেই" কোনো error নয়, স্বাভাবিক উত্তর। দুটিকে আলাদা রাখুন।

৯ · Browser-এ fetch: TanStack Query এখনো কোথায় মানায়

Server Component আসার পর browser থেকে fetch করার দরকার অনেক কমেছে। তবু কিছু কাজ browser-এরই: কয়েক সেকেন্ড পরপর স্টক দেখা (polling), infinite scroll, ব্যবহারকারী টাইপ করার সাথে সাথে খোঁজা, বা optimistic update. এসব জায়গায় পর্ব ১৯-এর (Server state — TanStack Query) TanStack Query এখনো সবচেয়ে কাজের। বাজারঘরের পণ্যের পাতায় স্টক প্রতি ১০ সেকেন্ডে browser থেকে নতুন করা হয়:

"use client";
import { useQuery } from "@tanstack/react-query";

// browser থেকে প্রতি ১০ সেকেন্ডে স্টক: এখানে TanStack Query এখনো মানানসই
export default function LiveStock({ id }: { id: number }) {
  const { data, isPending } = useQuery({
    queryKey: ["stock", id],
    queryFn: () => fetch(`/api/stock/${id}`).then((r) => r.json()),
    refetchInterval: 10_000,
  });
  return <p>স্টক: {isPending ? "…" : `${data.stock}টি`}</p>;
}

এই component @tanstack/react-query 5.105.2-সহ build-এ সফলভাবে compile হয়েছে, আর server-এ তৈরি HTML-এ এর জায়গায় "স্টক: …" ছিল, কারণ প্রথম ডেটা browser-এ আসে। এখানে browser-এর আচরণ চালিয়ে দেখানো হয়নি; পর্ব ১৯-এ সেটি বিস্তারিত চালানো আছে।

প্রয়োজনভালো জায়গা
পাতা খোলার সময়ই লাগে, SEO-তে দরকার (পণ্যের নাম, দাম)Server Component, দরকারে "use cache"
ধীর কিন্তু প্রথম দেখায় জরুরি নয় (রিভিউ)Server Component + <Suspense>
ব্যবহারকারীর কাজে বারবার বদলায় (খোঁজা, polling, infinite scroll)Client Component + TanStack Query
ডেটা বদলানো (দাম, কার্ট)Server Action (পর্ব ৫৪) + updateTag বা revalidateTag

আরও গভীরে: HTML টুকরো করে পাঠানোর ধারণা নতুন নয়। HTTP/1.1-এর chunked transfer coding এখন RFC 9112-এ সংজ্ঞায়িত, আর HTTP/2 ও HTTP/3-এ একই কাজ করে নিজস্ব frame. Facebook ২০১০ সালের দিকে "BigPipe" নামে পাতাকে "pagelet"-এ ভাগ করে stream করার কৌশল প্রকাশ করেছিল বলে জানা যায়; React 18 (২০২২) Suspense-ভিত্তিক streaming SSR এনে একই চিন্তাকে component-এর স্তরে নিয়ে আসে। stale-while-revalidate নামটিও HTTP থেকে নেওয়া: Mark Nottingham-এর RFC 5861 (২০১০) Cache-Control-এ এই নির্দেশ যোগ করে। Next.js-এর revalidateTag(tag, 'max') সেই একই ধারণা server-এর ভেতরের cache-এ প্রয়োগ করে।

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

  • Request waterfall কী, আর কীভাবে ভাঙবেন? স্বাধীন await একটির পর একটি চলা; Promise.all দিয়ে একসাথে শুরু করুন, মোট সময় Σ থেকে max-এ নামে।
  • React cache() আর "use cache"-এর পার্থক্য? প্রথমটি একটি অনুরোধের ভেতরে dedupe, দ্বিতীয়টি অনেক অনুরোধ জুড়ে টিকে থাকা cache.
  • revalidateTag বনাম updateTag? প্রথমটি stale-while-revalidate, Route handler-এও চলে; দ্বিতীয়টি সঙ্গে সঙ্গে বাতিল, শুধু Server Action-এ।
  • Streaming মোট সময় কমায়? না; দেখার মতো প্রথম অংশ আগে আসে, ধীরতম query-র সময় থেকেই যায়।
  • Stream শুরুর পরে error হলে HTTP status কী হয়? আগের header-এর ২০০ থেকে যায়; error boundary fallback দেখায় আর browser পায় digest.

১০ · AI-এর যুগে: data fetching আর cache-এর কোড যাচাই

Next.js-এর cache নিয়ে AI-এর ভুল খুব বেশি, কারণ তার শেখার ডেটায় তিনটি আলাদা যুগের কোড মেশানো: getServerSideProps-এর pages router, App Router-এর শুরুর "সব fetch cache" যুগ, আর এখনকার "use cache". ফলে এমন কোড আসে যা চলে, কিন্তু আপনার version-এ ভুল আচরণ করে।

AI যা লেখেসমস্যাকীভাবে ধরবেন
export const revalidate = 60, unstable_cacheআগের মডেল; Cache Components-এ মেলে নাnext.config-এ cacheComponents দেখুন, docs-এর version মেলান
revalidateTag('products')এক argument, deprecatedTypeScript error আর docs
Server Component থেকে fetch('http://localhost:3000/api/…')বাড়তি hop, build-এ server থাকে নাডেটা-ফাংশন সরাসরি ডাকুন
"use cache"-এর ভেতরে cookies() বা ব্যবহারকারীর ডেটাএকজনের ডেটা আরেকজন দেখতে পারেনCache key-তে কী আছে পড়ুন, দুই ব্যবহারকারী দিয়ে পরীক্ষা
একটির পর একটি awaitWaterfallLog-এ সময় মাপুন
try/catch-এ error গিলে খালি তালিকা ফেরানোভাঙন লুকিয়ে যায়, ক্রেতা ভাবেন পণ্য নেইerror.js আর log রাখুন

AI-কে প্রশ্ন করার সময় version, config আর মাপা সংখ্যা দিয়ে দিন, আর প্রমাণ চান:

আমি Next.js 16.4.4 ব্যবহার করছি, next.config-এ cacheComponents: true আছে। পণ্যের পাতায় তিনটি query: পণ্য (২০০ ms), স্টক (৪০০ ms), রিভিউ (১২০০ ms). পাতা এখন ১.৮ সেকেন্ড নেয়। ১. শুধু Next.js 16-এর Cache Components-এর API ব্যবহার করো। pages router, export const revalidate বা unstable_cache দিও না। ২. কোন অংশ "use cache" হবে, কোনটি Suspense-এ যাবে, আর কেন, তা বলো। ব্যবহারকারীর cookie পড়া কোনো ফাংশন cache কোরো না। ৩. দাম বদলের পর কোথায় revalidateTag(tag, 'max') আর কোথায় updateTag লাগবে, তা আলাদা করে বলো। ৪. প্রতিটি API-র জন্য nextjs.org docs-এর কোন পাতা দেখব, তার নাম দাও। কোনো নতুন package যোগ কোরো না।

AI-এর কোড হাতে পেলে যাচাইয়ের চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর উত্তর খুলুন।

প্র ০১Next.js-এর cache-এর default বারবার কেন বদলাল? "নিজে থেকে cache" ভালো, নাকি "স্পষ্ট করে cache"?

App Router যখন এলো (Next.js 13, ২০২২), নকশার লক্ষ্য ছিল যতটা সম্ভব static. তাই server-এর fetch ডিফল্টে cache হতো, আর একটি পাতা নিজে থেকে static নাকি dynamic হবে তা Next.js অনুমান করত। কাগজে এটি দারুণ: কিছু না লিখেই দ্রুত সাইট। বাস্তবে অনেক ডেভেলপার বিভ্রান্ত হলেন। ডেটাবেসে দাম বদলেছে, পাতায় আসছে না; কেউ বুঝতে পারছেন না কোন স্তরে আটকে আছে। কোন fetch cache হলো আর কোনটি হলো না, তা নির্ভর করত সেটি কোনো cookies() ডাকার আগে না পরে এসেছে তার ওপর। এমন নিয়ম কোড পড়ে বোঝা কঠিন।

Next.js 15 (২০২৪) তাই default উল্টে দেয়: fetch আর GET route handler ডিফল্টে cache হয় না। Next.js 16 আরও এক ধাপ যায়: Cache Components-এ cache একটি স্পষ্ট নির্দেশ, "use cache", আর তার আয়ু cacheLife-এ লেখা। Build নিজেই আপনাকে বাধ্য করে প্রতিটি ধীর অংশের জন্য সিদ্ধান্ত নিতে: cache করবেন, Suspense-এ রাখবেন, নাকি জেনেশুনে পাতাকে অপেক্ষা করাবেন। অংশ ৬-এর build error ঠিক সেই প্রশ্নই করেছিল।

এটি সফটওয়্যারের পুরোনো একটি বিতর্ক: implicit বনাম explicit. Implicit নিয়ম শুরুতে সহজ, কিন্তু ভুল হলে খুঁজে পাওয়া কঠিন। Explicit নিয়মে কিছু শব্দ বেশি লিখতে হয়, কিন্তু কোড পড়েই আচরণ বোঝা যায়, আর code review-তে ভুল ধরা পড়ে। Cache-এর ভুলের খরচ বড়: ভুল দাম, বা একজনের তথ্য আরেকজনের কাছে। তাই অনেকেই মনে করেন এখানে স্পষ্টতার দাম দেওয়া উচিত। উল্টো মতও আছে: ছোট সাইটে প্রতিটি ফাংশনে নির্দেশ লেখা বাড়তি ঝামেলা, আর নতুনদের জন্য Suspense-এর নিয়ম শিখতে সময় লাগে।

মূল উপলব্ধি: Framework-এর default বদলায়, তাই কোনো টিউটোরিয়ালের কোড নেওয়ার আগে তার version দেখুন। আর যে আচরণ আপনার ব্যবসার জন্য গুরুত্বপূর্ণ, যেমন দাম কতক্ষণ পুরোনো থাকতে পারে, তা কোডে স্পষ্ট করে লিখুন, default-এর ওপর ছেড়ে দেবেন না।

প্র ০২Streaming-এ পাতা আগে দেখা যায়, তাহলে সব পাতা কেন stream করা হয় না? এর খরচ কোথায়?

Streaming-এর লাভ আমরা মেপেছি: নাম আর দাম ৪১ ms-এ, Suspense ছাড়া ১,২৪৩ ms-এ। কিন্তু প্রতিটি সুবিধার দাম আছে।

  • Status code আটকে যায়: প্রথম টুকরো যাওয়ার সাথে header-এ 200 চলে যায়। পরে কিছু ভাঙলে ৫০০ পাঠানো যায় না, আমাদের পণ্য ৩-এর run-এ ঠিক তাই হলো। তাই "পণ্য আছে কি না" এমন সিদ্ধান্ত, যা status বদলায় (৪০৪), Suspense-এর বাইরে নেওয়া ভালো।
  • Layout লাফানো: Fallback আর আসল অংশের মাপ আলাদা হলে পাতা নিচে সরে যায়। এতে Core Web Vitals-এর CLS খারাপ হয়। Fallback-কে আসল অংশের কাছাকাছি মাপের skeleton বানানো এর প্রতিকার।
  • অনেক fallback বিরক্তিকর: দশটি ছোট অংশ দশবার "লোড হচ্ছে" দেখালে পাতা অস্থির লাগে। কয়েকটি অর্থপূর্ণ সীমারেখাই যথেষ্ট।
  • কিছু bot: Next.js-এর docs বলে, কিছু crawler metadata <head>-এ দেখতে চায়, তাই Next.js user agent দেখে তাদের জন্য streaming না করে পুরো পাতা একবারে দেয়। SEO-র বিস্তারিত পর্ব ৫৮-তে।
  • মধ্যস্থ সার্ভার: কোনো proxy বা compression স্তর উত্তর জমিয়ে একবারে পাঠালে streaming-এর লাভ মুছে যায়। তাই production-এ নিজে মেপে দেখা দরকার।

ইতিহাসেও এই trade-off দেখা যায়। Facebook-এর BigPipe (প্রায় ২০১০) পাতাকে টুকরোয় ভাগ করে stream করত, আর React 18 সেই চিন্তা component-এর স্তরে এনেছে। দুই ক্ষেত্রেই নকশাকারীদের বেছে নিতে হয়েছে কোন অংশ আগে জরুরি।

মূল উপলব্ধি: Streaming ধীর অংশকে দ্রুত করে না, শুধু অপেক্ষাটাকে ক্রেতার চোখ থেকে সরায়। সীমারেখা বসান সেখানে, যেখানে ব্যবহারকারী আংশিক পাতা দিয়েও কাজ শুরু করতে পারেন।

প্র ০৩Server Component সরাসরি ডেটাবেস পড়তে পারে, তাহলে আলাদা API রাখার দরকার কি শেষ?

পুরোপুরি নয়। কে ডেটা চাইছে, প্রশ্নটা সেখানে।

বাজারঘরের নিজের পাতার জন্য API-কে HTTP দিয়ে ডাকা অপচয়। আমাদের run-এ একই কম্পিউটারে ২৫ ms বাড়তি লাগল; আলাদা server-এ ১০–৫০ ms বা তার বেশি হতে পারে, আর build-এর সময় সেই server চালুই থাকে না। এর বদলে একটি ডেটা-স্তর (data access layer) রাখুন: getProduct, getProducts-এর মতো ফাংশন, যার ভেতরে permission যাচাই আর query দুটোই আছে। Server Component সেটি সরাসরি ডাকে।

কিন্তু বাজারঘরের যদি একটি Android অ্যাপ থাকে, বা কোনো ডেলিভারি পার্টনার স্টক দেখতে চায়, তাদের তো আপনার React component চালানোর উপায় নেই। তাদের জন্য একটি স্থির HTTP চুক্তি লাগে: REST (পর্ব ৩২), GraphQL (পর্ব ৩৬) বা Route handler. পেমেন্ট গেটওয়ের webhook-ও একটি URL চায়। Browser-এর polling-ও (অংশ ৯) HTTP-ই ব্যবহার করে।

ভালো নকশা হলো দুই দরজা, এক ভাঁড়ার। Server Component আর Route handler দুজনেই একই ডেটা-স্তরের ফাংশন ডাকে, যাতে নিয়ম একবারই লেখা থাকে। আমাদের run-এ আরেকটি শিক্ষাও ছিল: পাতা আর Route handler আলাদা bundle-এ, তাই module-এর ভেতরের ভ্যারিয়েবল ভাগ হয় না। ভাগ করা state রাখুন ডেটাবেসে বা Redis-এ, memory-তে নয়।

একটি বিতর্কও আছে। কেউ কেউ মনে করেন UI কোডের এত কাছে ডেটাবেস query থাকলে স্তরের সীমা ঝাপসা হয়, আর ছোট দলে নিরাপত্তার ভুল সহজে ঢোকে। অন্যরা বলেন, ডেটা-স্তর পরিষ্কার থাকলে এটি সবচেয়ে সরল ও দ্রুত পথ। দুই পক্ষই একমত যে permission যাচাই UI component-এ ছড়িয়ে না রেখে ডেটা-স্তরে রাখতে হবে।

মূল উপলব্ধি: নিজের পাতার জন্য ফাংশন ডাকুন, বাইরের জগতের জন্য API রাখুন, আর দুটিকে একই ডেটা-স্তরের ওপর দাঁড় করান।

প্র ০৪"Cache invalidation কঠিন" কথাটি এত প্রচলিত কেন? revalidateTag আর updateTag সেই সমস্যার কোন অংশ সমাধান করে?

Phil Karlton-এর নামে একটি উক্তি প্রচলিত: কম্পিউটার বিজ্ঞানে কঠিন কাজ দুটি, cache invalidation আর নাম দেওয়া। কঠিনতার কারণ হলো cache মানে একই তথ্যের দুটি কপি, আর দুই কপিকে একসাথে বদলানোর কোনো বিনামূল্যের উপায় নেই।

তিনটি প্রশ্ন সবসময় থাকে। কোন কপি পুরোনো হলো? ট্যাগ এখানে সাহায্য করে: getProduct(1)-এর গায়ে products আর product-1 দুটি ট্যাগ থাকলে দাম বদলে দুটিকেই ধরা যায়। কিন্তু ট্যাগ লাগানো ভুলে গেলে Next.js জানবে না। পুরোনো হওয়ার পর কী দেখাব? revalidateTag(tag, 'max') বলে "পুরোনোটা দাও, পেছনে নতুন বানাও", HTTP-র RFC 5861-এর stale-while-revalidate-এর মতো। এতে কেউ অপেক্ষা করে না, কিন্তু অন্তত একজন পুরোনো দাম দেখেন। updateTag বলে "এখনই ফেলে দাও", যাতে যিনি বদলালেন তিনি নিজের বদল দেখেন (read-your-own-writes). একাধিক server থাকলে? প্রতিটি instance-এর নিজের memory-র cache থাকলে একটিতে বাতিল করলে অন্যটিতে পুরোনো থেকে যেতে পারে। তখন ভাগ করা cache handler বা পর্ব ৪৪-এর Redis-এর মতো একটি কেন্দ্রীয় জায়গা লাগে।

একটি race-ও আছে, পর্ব ৪৪-এ যা দেখেছেন: পুরোনো ডেটা পড়া অনুরোধ যদি বাতিলের পরে cache-এ লেখে, পুরোনো দাম আবার ফিরে আসে। Framework এর কিছুটা সামলায়, কিন্তু আপনার নিজের cache-এ এই ভুল সহজ।

ব্যবসার দিক থেকে প্রশ্নটা সহজ ভাষায়: কত সেকেন্ড পুরোনো দাম দেখালে ক্ষতি নেই? ক্যাটালগে কয়েক মিনিট হয়তো চলে, কিন্তু চেকআউটের মোট টাকা কখনো cache থেকে নয়, সবসময় ডেটাবেস থেকে।

মূল উপলব্ধি: ট্যাগ বলে কী পুরোনো হলো, আর revalidateTag বনাম updateTag বলে পুরোনো হওয়ার পর কী দেখাবেন. দুটি সিদ্ধান্তই আলাদা করে নিতে হয়।

প্র ০৫Serverless-এ চালালে "use cache"-এর hit rate কেন কমে যেতে পারে, আর deploy করলে cache কেন খালি হয়?

Next.js-এর docs অনুযায়ী "use cache"-এর default store প্রতিটি server instance-এর নিজের memory-তে থাকে। আমাদের run-এ একটিমাত্র next start process, তাই দ্বিতীয় অনুরোধেই hit. Serverless প্ল্যাটফর্মে ব্যাপারটা আলাদা: ভিড়ে নতুন instance চালু হয়, অলস হলে বন্ধ হয়। প্রতিটি নতুন instance খালি memory নিয়ে শুরু করে, তাই প্রথম অনুরোধ miss. Instance যত বেশি আর যত ঘনঘন বদলায়, hit rate তত কমে।

একটি হিসাব (অনুমিত সংখ্যা): ঘণ্টায় ১০,০০০ অনুরোধ, ২০টি instance, প্রতিটির আয়ু গড়ে ১৫ মিনিট। তাহলে ঘণ্টায় প্রায় ২০ × ৪ = ৮০টি নতুন instance, প্রতিটিতে অন্তত একটি miss. Hit rate (১০,০০০ − ৮০) ÷ ১০,০০০ = ৯৯.২%. এখনো ভালো, কিন্তু query ধীর আর ট্রাফিক কম হলে এই miss-গুলোই চোখে পড়ে। এজন্য docs "use cache: remote" দেয়, যা একটি ভাগ করা cache handler-এ রাখে। এর খরচ প্রতিটি পড়ায় একটি নেটওয়ার্ক hop, তাই docs নিজেই বলে লাভ হয় কেবল hit rate বেশি হলে।

Deploy-এর প্রশ্নের উত্তরও docs-এ আছে: cache key-তে build id থাকে। নতুন deploy মানে নতুন কোড, আর পুরোনো কোডের বানানো ফল নতুন কোডের সাথে না-ও মিলতে পারে। Component-এর আকার বদলালে পুরোনো RSC payload ভুল UI দেখাত। তাই প্রতিটি deploy খালি cache নিয়ে শুরু করে, এমনকি remote cache-ও। এর মানে deploy-এর ঠিক পরে ডেটাবেসে চাপ বাড়ে; জনপ্রিয় পাতার shell build-এ বানিয়ে রাখা (generateStaticParams) সেই চাপ কমায়।

মূল উপলব্ধি: Cache-এর কার্যকারিতা কোডে নয়, চালানোর পরিবেশে ঠিক হয়। কোথায় deploy করছেন, কয়টি instance চলে, কত ঘনঘন deploy হয়, এসব জেনে hit rate মাপুন, অনুমান করবেন না।

অনুশীলন

হিসাব, পর্যালোচনা, ফল অনুমান আর একটি আসল কমান্ড।

  1. সময়ের হিসাব: বাজারঘরের "আমার অর্ডার" পাতায় চারটি query: ব্যবহারকারী (১৫০ ms), সেই ব্যবহারকারীর অর্ডার (২৫০ ms, ব্যবহারকারীর id লাগে), সুপারিশ (৯০০ ms, স্বাধীন), ব্যানার (১০০ ms, স্বাধীন)। (ক) সব একটির পর একটি চালালে মোট কত? (খ) সবচেয়ে ভালো ক্রমে কত? (গ) সুপারিশকে Suspense-এ রাখলে অর্ডারের তালিকা কখন দেখা যাবে?

    (ক) ১৫০ + ২৫০ + ৯০০ + ১০০ = ১,৪০০ ms.

    (খ) ব্যবহারকারী → অর্ডার একটি শিকল: ১৫০ + ২৫০ = ৪০০ ms. এই শিকল, সুপারিশ আর ব্যানার একসাথে: max(৪০০, ৯০০, ১০০) = ৯০০ ms. সাশ্রয় ৫০০ ms, প্রায় ৩৬%.

    (গ) সুপারিশ আলাদা Suspense-এ থাকলে অর্ডারের অংশ অপেক্ষা করে শুধু নিজের শিকলের জন্য: max(৪০০, ১০০) = প্রায় ৪০০ ms, তারপর সুপারিশ ৯০০ ms-এ stream হয়ে আসে।

  2. পর্যালোচনা করুন: একজন সহকর্মী নিচের পাতাটি লিখেছেন (Next.js 16, cacheComponents: true). অন্তত চারটি সমস্যা খুঁজুন: export default async function Page() { const user = (await cookies()).get('uid')?.value; const res = await fetch('http://localhost:3000/api/products'); const products = await res.json(); const reviews = await getReviews(); return …; }, আর getReviews-এর ভেতরে "use cache" আছে কিন্তু ট্যাগ নেই। দাম বদলের route-এ লেখা revalidateTag('products').
    • cookies() পাতার একেবারে ওপরে, Suspense-এর বাইরে: build ব্যর্থ হবে ("uncached or runtime data during prerendering"). Cookie-নির্ভর অংশ আলাদা component-এ নিয়ে Suspense-এ রাখুন।
    • নিজের API-কে localhost দিয়ে ডাকা: বাড়তি hop, production-এ ভুল ঠিকানা, আর build-এ server চালু নেই। ডেটা-স্তরের getProducts() সরাসরি ডাকুন, তাতে "use cache" আর cacheTag('products') দিন।
    • তিনটি await একটির পর একটি: products আর reviews স্বাধীন, Promise.all বা আলাদা Suspense লাগবে।
    • getReviews-এ ট্যাগ নেই: নতুন রিভিউ এলে on-demand নতুন করার উপায় নেই, শুধু cacheLife-এর সময়ের অপেক্ষা। cacheTag('reviews') আর একটি স্পষ্ট cacheLife দিন।
    • revalidateTag('products') এক argument, deprecated: revalidateTag('products', 'max') লিখুন, অথবা Server Action হলে updateTag.
    • বোনাস: route-টিতে অনুমতি যাচাই আছে কি না দেখুন।
  3. ফল অনুমান করুন: একটি cache-এ শুরুতে getProducts-এর জমা ফল নেই, ডেটাবেসে দাম ৫০০. ক্রম: অনুরোধ A; অনুরোধ B; দাম ৫২০ করা আর revalidateTag('products','max'); অনুরোধ C; পেছনের কাজ শেষ; অনুরোধ D; দাম ৫৫০ করা আর updateTag('products'); অনুরোধ E. প্রতিটি অনুরোধ কোন দাম দেখবে, আর মোট কয়টি ডেটাবেস query হবে?

    A: miss, ৫০০ (query ১)। B: hit, ৫০০. C: stale, ৫০০, পেছনে query ২ চলে ৫২০ জমা করে। D: hit, ৫২০. E: updateTag জমাটি ফেলে দিয়েছে, তাই miss, ৫৫০ (query ৩)। মোট ৩টি query. পাতার tag-cache-model.js-এর আসল আউটপুটও এই ধাঁচেই: MISS, HIT, STALE, HIT, MISS, HIT.

  4. Route table পড়ুন: নিজের একটি Next.js 16 প্রকল্পে npx next build চালান। প্রতিটি route-এর পাশের ○, ◐, ƒ চিহ্ন আর Revalidate/Expire কলাম লিখে রাখুন। তারপর একটি পাতায় Suspense-এর বাইরে await headers() যোগ করে আবার build দিন। কী হয়, আর দুটি সমাধান কী?

    ○ মানে পুরো পাতা build-এ তৈরি, ◐ মানে static shell আছে আর কিছু অংশ stream হবে, ƒ মানে প্রতিটি অনুরোধে server-এ তৈরি। Revalidate/Expire কলাম আসে সেই পাতার "use cache"-এর cacheLife থেকে, যেমন hours দিলে 1h আর 1d.

    Suspense-এর বাইরে headers() দিলে Cache Components চালু থাকলে build ব্যর্থ হয়: Route "/…": Next.js encountered uncached or runtime data during prerendering. সমাধান: (১) header-নির্ভর অংশ আলাদা component-এ নিয়ে <Suspense>-এ রাখা, ফলে পাতা ◐ থাকে; (২) পুরো পাতাকে জেনেশুনে অপেক্ষা করাতে export const instant = false, ফলে পাতা ƒ হয় আর কিছুই আগে দেখা যায় না।

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

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

ক্রমSuspense দিয়ে stream হওয়া একটি পণ্যের পাতার ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📨 Browser পাতাটির জন্য GET অনুরোধ পাঠায়
  2. 🧱 Server নাম, দাম আর রিভিউর fallback সহ প্রথম HTML টুকরো পাঠায়
  3. ⏳ Server-এ রিভিউর ধীর query চলতে থাকে
  4. 💬 একই উত্তরের ভেতরে লুকানো div-এ রিভিউর HTML আসে
  5. 🔁 ছোট একটি script fallback সরিয়ে রিভিউ বসায়

তিনটি স্বাধীন query-র সময় ২০০, ৪০০ আর ১২০০ ms. একটির পর একটি await করলে আর Promise.all দিয়ে চালালে মোট সময় প্রায় কত?

  • দুই ক্ষেত্রেই ১২০০ ms
  • ১৮০০ ms আর ১২০০ ms
  • ১৮০০ ms আর ৬০০ ms
  • ৬০০ ms আর ২০০ ms

Next.js 16-এ cacheComponents: true. একটি ফাংশনে "use cache" আর cacheTag('products') আছে। দাম বদলের পর একটি Route handler revalidateTag('products', 'max') ডাকল। ঠিক পরের অনুরোধে কী ঘটে?

  • অনুরোধটি নতুন ডেটার জন্য অপেক্ষা করে, তারপর নতুন দাম পায়
  • cacheLife শেষ না হওয়া পর্যন্ত পুরোনো দামই চলতে থাকে
  • অনুরোধটি পুরোনো দাম পায়, আর পেছনে নতুন ডেটা তৈরি হয়
  • Route handler-এ revalidateTag চলে না, তাই কিছুই বদলায় না

Next.js 16-এ updateTag কোথা থেকে ডাকা যায়?

  • শুধু Server Action থেকে
  • Server Action আর Route handler দুই জায়গা থেকেই
  • যেকোনো Client Component থেকে
  • যেকোনো Server Component-এর render-এর ভেতর থেকে

একটি ফাংশন React-এর cache() দিয়ে মোড়া। একটি পাতার render-এ তিনটি component একই argument দিয়ে এটি ডাকল, আর পরের অনুরোধেও আবার তিনবার ডাকা হলো। দুই অনুরোধে মোট কয়টি ডেটাবেস query যাবে?

  • ৬টি
  • ১টি
  • ০টি
  • ২টি

Next.js 16-এ Cache Components চালু। একটি page-এর একেবারে ওপরে, কোনো Suspense ছাড়া, await cookies() ডাকা হলো, আর instant সেট করা নেই। next build-এ কী ঘটে?

  • পুরো route চুপচাপ dynamic (ƒ) হয়ে যায়
  • Build ব্যর্থ হয়, error-এ Suspense, use cache বা instant = false-এর কথা বলা থাকে
  • Build-এর সময়কার cookie দিয়ে পাতা static হয়ে যায়
  • TypeScript error হয়, কারণ cookies() async নয়

একটি streamed পাতার প্রথম টুকরো চলে যাওয়ার পর Suspense-এর ভেতরের একটি Server Component error ছুড়ল, আর সেই segment-এ error.js আছে। Production-এ browser কী পায়?

  • HTTP 500 status আর error-এর পুরো বার্তা
  • ফাঁকা সাদা পাতা
  • HTTP 200 থেকে যায়, error.js-এর fallback আসে, আর বার্তার বদলে শুধু একটি digest
  • HTTP 200 আর error-এর পুরো বার্তা ও stack trace

বাজারঘরের পণ্যের পাতায় প্রতি ১০ সেকেন্ডে স্টক নতুন করে দেখাতে হবে, পাতা reload না করে। কোন পথ সবচেয়ে মানানসই?

  • Client Component-এ TanStack Query, refetchInterval দিয়ে, একটি Route handler থেকে
  • স্টকের ফাংশনে "use cache" আর cacheLife("seconds")
  • স্টকের ফাংশনকে React cache() দিয়ে মোড়ানো
  • পণ্যের ফোল্ডারে একটি loading.js যোগ করা

শব্দকোষ

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

মনে রাখুন

  1. Server Component-এ সরাসরি ডেটা-স্তরের ফাংশন ডাকুন; নিজের API-কে HTTP দিয়ে ডাকা বাড়তি hop.
  2. স্বাধীন query Promise.all দিয়ে একসাথে: মোট সময় Σ থেকে max-এ নামে। একই ডেটা বারবার লাগলে React cache().
  3. Next.js 16-এ cache স্পষ্ট: "use cache" + cacheLife + cacheTag. পুরোনো টিউটোরিয়ালের default আর খাটে না।
  4. বদলের পর revalidateTag(tag, 'max') পুরোনোটা একবার দেয়, updateTag সঙ্গে সঙ্গে বাতিল করে (শুধু Server Action).
  5. cookies(), headers(), searchParams আর ধীর অংশ <Suspense>-এ: পাতা আগে দেখা যায়; ভাঙলে error.js, আর browser পায় শুধু digest.

পরের পর্বে: আজ ডেটা পড়লেন আর cache বাতিল করলেন একটি Route handler থেকে। এবার form থেকে সরাসরি server-এ ডেটা বদলানোর পথ, Server Action: কীভাবে লিখবেন, কীভাবে updateTag দিয়ে নিজের বদল সঙ্গে সঙ্গে দেখাবেন, আর কীভাবে একে আক্রমণ থেকে রক্ষা করবেন।

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

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