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

Hydration — server থেকে client-এ, mismatch আর খরচ

Hydration — From Server HTML to an Interactive Client, Mismatches and Cost

Taught in Bangla, this lesson explains hydration with hydrateRoot, measures the dead gap between seeing a page and the first working click on a slowed CPU, reproduces real mismatch errors from dates, random values, typeof window, locale and bad nesting, fixes them in order, uses useSyncExternalStore with getServerSnapshot, selective hydration with Suspense, server components to cut JavaScript, islands and resumability, and a hydration budget.

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

  • Server HTML আর client JavaScript কীভাবে জোড়া লাগে, আর hydrateRoot ঠিক কী মেলায়
  • “দেখা যায় কিন্তু কাজ করে না” সময়টি ধীর CPU-তে মাপা, আর ক্লিক কখন হারায়, কখন লাইনে দাঁড়ায়
  • ছয় রকম hydration mismatch-এর আসল Next.js error পড়া আর ঠিক করা
  • Suspense দিয়ে selective hydration আর ক্লিকের অগ্রাধিকার, একটি আসল run-এ
  • বড় পাতার hydration খরচ হিসাব করে “বাজারঘর”-এর পণ্যের পাতার জন্য একটি বাজেট বানানো

১ · Hydration কী: ছবির মতো পাতায় প্রাণ জোড়া

পর্ব ০৩-এ rendering-এর মানচিত্র দেখেছেন: CSR, SSR, SSG আর মাঝখানে hydration-এর একটি ছোট ঝলক। পর্ব ৫২-তে server আর client component-এর ভাগ, আর পর্ব ৫৫-তে কোন পাতা কখন তৈরি হয়, সেটি আলাদা পর্বে আছে। এই পর্বে আমরা ঠিক জোড়ার জায়গাটিতে নামব: HTML পৌঁছানোর পর ব্রাউজারে কী ঘটে, কোথায় ভাঙে, আর কত খরচ হয়।

সকাল ন’টায় একটি মুদি দোকানের শাটার উঠল। তাকে সব পণ্য সাজানো, দামের ট্যাগও লাগানো। ক্রেতা ঢুকে চাল হাতে নিলেন। কিন্তু ক্যাশিয়ার এখনো রাস্তায়। পণ্য দেখা যায়, দাম পড়া যায়, কিন্তু টাকা দেওয়া যায় না। ক্যাশিয়ার এসে আগে হিসাব খাতা মেলান: তাকে যা আছে, খাতাতেও তাই আছে কি না। মিললে কাউন্টার খোলে। না মিললে আবার গুনতে বসেন। Server-এর HTML হলো সাজানো তাক। ব্রাউজারের JavaScript হলো ক্যাশিয়ার। আর খাতা মেলানো থেকে কাউন্টার খোলা পর্যন্ত কাজটিই hydration.

যান্ত্রিকভাবে তিনটি ধাপ। প্রথমে server component tree চালিয়ে HTML বানায়, যেমন <button>কার্টে যোগ (0)</button>. HTML-এ শুধু চেহারা থাকে; onClick-এর function HTML-এ লেখা যায় না। দ্বিতীয়ত, ব্রাউজার HTML আঁকে, আর পাশাপাশি JavaScript নামায়। তৃতীয়ত, JavaScript চলে, React একই component আবার render করে। তবে এবার নতুন DOM বানায় না। পর্দায় যে DOM আছে, তার প্রতিটি node-এর সাথে নিজের element মেলায়, আর মিলে গেলে সেই node-এ handler জোড়ে।

React-এ এই কাজের API হলো hydrateRoot, যা react-dom/client থেকে আসে। পর্ব ০৭ থেকে যে createRoot চেনেন, সেটি খালি জায়গায় নতুন DOM বানায়। hydrateRoot আগে থেকে থাকা DOM গ্রহণ করে।

import { hydrateRoot } from 'react-dom/client';
import App from './App';

const root = hydrateRoot(document.getElementById('root'), <App />, {
  onRecoverableError(error, info) {
    // React নিজে সামলেছে, তবু বাগ: log পাঠান
    reportToServer(error.message, info.componentStack);
  },
});

react.dev অনুযায়ী স্বাক্ষর hydrateRoot(domNode, reactNode, options?). Option-এর মধ্যে onRecoverableError গুরুত্বপূর্ণ: React নিজে থেকে কোনো error সামলে নিলে এটি ডাকা হয়, আর hydration mismatch ঠিক এই দলে পড়ে। identifierPrefix server আর client-এ এক হতে হয়, কারণ পর্ব ১৪-এর useId দুই দিকে একই id বানাতে এটি ব্যবহার করে। Next.js-এ এই ফাইল আপনি লেখেন না; framework নিজেই hydrateRoot ডাকে। কিন্তু আচরণটি একই, তাই ভেতরটা জানা দরকার।

মূল চুক্তি

Hydration-এর প্রথম render-এ client-কে হুবহু সেই tree বানাতে হবে, যা server বানিয়েছিল। react.dev সরাসরি বলে: mismatch-কে বাগ হিসেবে ধরুন। ভালো ক্ষেত্রে শুধু ধীর হয়, খারাপ ক্ষেত্রে handler ভুল element-এ জুড়ে যেতে পারে।

একটি পণ্যের পাতার সময়রেখা

One local run: Edge, CPU 6× slower, 150 ms latency, 1.6 Mbps

HTML আঁকা, JavaScript চলা আর ক্লিক কাজ করার সময় পর্দা সাদা পণ্য, দাম, বোতাম দেখা যায় Main thread JS নামছে long task ক্লিক হারায় লাইনে চলে 0 500 1000 1500 ms
ছবি ৫৬.১ — একটি local run-এর মধ্যমা আর প্রথম run-এর long task. পর্দা আগে ভরে, হাত পরে কাজ করে। মাঝের ফাঁকটিই এই পর্বের মূল সমস্যা।

React ২০১৩ সালে প্রকাশ হয়, আর শুরু থেকেই server-এ HTML বানানো যেত। React 16 (২০১৭) ReactDOM.hydrate আলাদা করে, আর React 18 (২০২২) আনে hydrateRoot, streaming SSR আর Suspense-ভিত্তিক selective hydration. React 19 (ডিসেম্বর ২০২৪) mismatch-এর জন্য অনেকগুলো আলাদা warning-এর বদলে একটি error-এ diff দেখানো শুরু করে, আর <head> ও <body>-এ বাইরের script বা extension-এর ঢোকানো অপ্রত্যাশিত tag কিছুটা সহ্য করতে শেখে। Next.js-এর যাত্রা ২০১৬ সালে, আর App Router-এ এই সব ধারণা একসাথে কাজ করে।

২ · মৃত মুহূর্ত: ধীর CPU-তে মাপা

কথায় নয়, মেপে দেখি। আমরা “বাজারঘর”-এর একটি পণ্যের পাতা বানালাম: জামদানি শাড়ি, দাম, একটি “কার্টে যোগ” বোতাম আর ৬০০টি রিভিউ। সত্যিকারের run: Next.js 16.4.4, React 19.3.1, next build তারপর next start, ব্রাউজার Microsoft Edge, Playwright দিয়ে চালানো। Chrome DevTools Protocol দিয়ে CPU ৬ গুণ ধীর, আর নেটওয়ার্ক 150 ms latency ও 1.6 Mbps. প্রতিবার নতুন context, তাই cache খালি। Script পাতা খোলার সাথে সাথে বোতামে বারবার ক্লিক করে, যতক্ষণ না সংখ্যা বাড়ে।

$ python measure.py 6 net
/product  CPU 6x throttle, net=150ms/1.6Mbps, 5 runs (median)
  first-contentful-paint : 520 ms
  hydrated (effect ran)  : 1360 ms
  long tasks run 1       : [[1079, 196], [1282, 114]]
  first successful click : 1418 ms
  clicks lost before that: [11, 14, 14, 10, 10]
  HTML transfer/decoded  : 16811 / 249956 bytes
  JS files, transfer/decoded: 6, 137277 / 457458 bytes
  each run fcp/click: [(496, 1418), (496, 1432), (520, 1408), (520, 1404), (576, 1459)]

কোন config-এ চালানো: এই পর্বের সব run একটি হাতে বানানো ছোট প্রজেক্টে (~/bazarghar), যার package.json-এ শুধু next, react আর react-dom, আর কোনো next.config ফাইল নেই। মানে cacheComponents বন্ধ, Next.js-এর ডিফল্ট। create-next-app@16.4.4 --yes দিয়ে বানানো প্রজেক্টে next.config.ts-এ cacheComponents: true থাকতে পারে; তখন কোন পাতা static বা dynamic হয়, আর connection() বা Suspense ছাড়া কিছু hook-এর আচরণ আলাদা হতে পারে (পর্ব ৫১ আর ৫৩ দেখুন)। Hydration-এর মূল নিয়ম দুই config-এ একই।

এটি একটি local run, সাধারণ ফল নয়; আপনার যন্ত্রে সংখ্যা আলাদা হবে। তবু ছবিটি পরিষ্কার। পাঁচবারের মধ্যমায় প্রথম লেখা পর্দায় আসে 520 ms-এ, যাকে বলে FCP. Component-এর useEffect-এ রাখা performance.mark('hydrated') চলে 1360 ms-এ। প্রথম সফল ক্লিক 1418 ms-এ। প্রায় 900 ms ধরে বোতাম দেখা যায়, কিন্তু কাজ করে না। এই সময়ে আমাদের script প্রতিটি run-এ ১০ থেকে ১৪ বার ক্লিক করেছে, একটিও গোনা হয়নি।

Long task-এর তালিকা দেখুন: 1079 ms-এ শুরু হয়ে 196 ms চলা একটি, আর 1282 ms-এ 114 ms-এর আরেকটি। JavaScript নামা শেষ হলে main thread এই সময়ে parse, চালানো আর hydration-এ ব্যস্ত। এর আগে thread প্রায় ফাঁকা ছিল, কিন্তু তখন বোতামে কোনো listener-ই ছিল না।

হারানো ক্লিক বনাম লাইনে দাঁড়ানো ক্লিক

একই পাতা নেটওয়ার্ক ধীর না করে, শুধু CPU ৬ গুণ ধীর করে চালালাম। তখন clicks lost হলো [0, 0, 0, 0, 0]. প্রথম ক্লিকটিই শেষ পর্যন্ত কাজ করেছে, যদিও দেরিতে। কারণ JavaScript দ্রুত এসেছে, ক্লিক পড়েছে এমন সময়ে যখন main thread hydration-এ ব্যস্ত। ব্রাউজার ব্যস্ত thread-এর জন্য input event লাইনে রাখে, task শেষ হলে পাঠায়। ততক্ষণে React handler জুড়ে ফেলেছে।

অর্থাৎ hydration-এর আগে ক্লিকের তিন রকম ভাগ্য হতে পারে। নিচের ব্লকটি একটি শিক্ষণ মডেল: সংখ্যাগুলো ওপরের run থেকে নেওয়া, গোল করা।

// শিক্ষণ মডেল: hydration-এর আগে ক্লিকের ভাগ্য (সময় ms-এ)
// সংখ্যাগুলো একটি local run-এর মধ্যমা থেকে নেওয়া, গোল করা
const t = { paint: 520, rootAttached: 1080, hydrated: 1360 };
const busy = [[1080, 1280], [1280, 1360]];   // long task: [শুরু, শেষ]
function busyUntil(ms) {
  const task = busy.find(([s, e]) => ms >= s && ms < e);
  return task ? task[1] : ms;
}
function fate(click) {
  if (click.at < t.paint) return 'পাতা তখনো সাদা, বোতামই নেই';
  if (click.at < t.rootAttached) {
    return click.native ? 'browser নিজে চালাল (link বা form)' : 'হারিয়ে গেল: listener নেই';
  }
  const start = busyUntil(click.at);           // ব্যস্ত থাকলে event লাইনে দাঁড়ায়
  const handled = Math.max(start, t.hydrated);  // React hydrate শেষে replay করে
  if (handled === click.at) return 'সঙ্গে সঙ্গে handler চলল';
  return `লাইনে ছিল, handler চলল ${handled} ms-এ (দেরি ${handled - click.at} ms)`;
}
const clicks = [
  { at: 300, what: 'কার্টে যোগ' },
  { at: 700, what: 'কার্টে যোগ' },
  { at: 900, what: 'পণ্যের link', native: true },
  { at: 1150, what: 'কার্টে যোগ' },
  { at: 1300, what: 'কার্টে যোগ' },
  { at: 1500, what: 'কার্টে যোগ' },
];
for (const c of clicks) console.log(`${String(c.at).padStart(4)} ms  ${c.what}: ${fate(c)}`);
console.log(`\nক্লিক হারায়: ${t.paint}–${t.rootAttached} ms (${t.rootAttached - t.paint} ms)`);
console.log(`ক্লিক দেরিতে চলে: ${t.rootAttached}–${t.hydrated} ms (${t.hydrated - t.rootAttached} ms)`);

আউটপুট:

 300 ms  কার্টে যোগ: পাতা তখনো সাদা, বোতামই নেই
 700 ms  কার্টে যোগ: হারিয়ে গেল: listener নেই
 900 ms  পণ্যের link: browser নিজে চালাল (link বা form)
1150 ms  কার্টে যোগ: লাইনে ছিল, handler চলল 1360 ms-এ (দেরি 210 ms)
1300 ms  কার্টে যোগ: লাইনে ছিল, handler চলল 1360 ms-এ (দেরি 60 ms)
1500 ms  কার্টে যোগ: সঙ্গে সঙ্গে handler চলল

ক্লিক হারায়: 520–1080 ms (560 ms)
ক্লিক দেরিতে চলে: 1080–1360 ms (280 ms)

700 ms-এর ক্লিক সবচেয়ে বিপজ্জনক: ব্যবহারকারী চাপলেন, কিছুই হলো না, কোনো চিহ্নও নেই। তিনি ভাবেন পাতা নষ্ট। 900 ms-এর <a href> কাজ করেছে, কারণ link আর form ব্রাউজারের নিজের আচরণ; JavaScript লাগে না। এটিই progressive enhancement-এর বাস্তব লাভ। 1150 আর 1300 ms-এর ক্লিক হারায়নি, দেরিতে চলেছে। মডেলটি সরল: আসল React-এ ক্লিক replay হয় শুধু নির্দিষ্ট পরিস্থিতিতে, যা অংশ ৬-এ আসল run দিয়ে দেখব।

  1. অনুরোধGET /product0 ms
  2. HTML আঁকাFCP520 ms
  3. JS নামছে137 kBgzip
  4. HydrationhydrateRoot1079 ms থেকে
  5. সচলonClick1418 ms
ব্যবহারকারীর ফোনে এই মুহূর্তে
সময়
পর্দায়
বোতাম চাপলে
Main thread
  1. 0 ms. ব্রাউজার অনুরোধ পাঠাল। 150 ms latency-তে সংযোগ আর উত্তরের পথে কয়েকবার যাতায়াত লাগে। পর্দা সাদা।
  2. 520 ms. HTML এল, ব্রাউজার আঁকল। শাড়ির নাম, দাম, বোতাম আর রিভিউ দেখা যায়। HTML-এ কোনো handler নেই, তাই চাপ দিলে কিছুই হয় না।
  3. 900 ms. 137 kB gzip করা JavaScript ধীর নেটওয়ার্কে নামছে। Main thread প্রায় ফাঁকা, তাই ক্লিক সঙ্গে সঙ্গে পৌঁছায়, কিন্তু শোনার কেউ নেই। ক্লিক হারায়।
  4. 1150 ms. JavaScript চলছে, React DOM মেলাচ্ছে। 196 ms-এর long task চলাকালীন ক্লিক এলে ব্রাউজার সেটি লাইনে রাখে। Task শেষে handler তৈরি থাকলে ক্লিক কাজ করে।
  5. 1418 ms. প্রথম সফল ক্লিক, সংখ্যা ১ হলো। HTML আসা থেকে প্রায় 900 ms পরে। এই ফাঁক কমানোই hydration অপ্টিমাইজেশনের লক্ষ্য।

নিজে মাপতে চাইলে: Edge বা Chrome-এর DevTools-এ Performance প্যানেলে CPU throttling আর Network throttling দিন, production build চালান, রেকর্ড করুন। Main সারিতে long task আর Interactions সারিতে ক্লিক দেখবেন। Development build-এ মাপবেন না: next dev-এ React-এর বাড়তি পরীক্ষা আর StrictMode-এর দ্বিগুণ effect সময় বদলে দেয়। বিস্তারিত মাপের পদ্ধতি পর্ব ২৩-এ।

৩ · Mismatch: Next.js dev-এর আসল error

Hydration-এর চুক্তি ভাঙলে কী হয়, দেখতে আমরা ছয়টি ছোট পাতা বানালাম। প্রতিটি একটি client component, কারণ server component ব্রাউজারে আবার চলে না, তাই তার ভেতরে এই সমস্যা এভাবে আসে না। সত্যিকারের run: Next.js 16.4.4 next dev (Turbopack), Edge-এ locale bn-BD আর time zone Asia/Dhaka. Console থেকে ধরা error নিচে; Next.js-এর ভেতরের লম্বা component stack “…” দিয়ে ছাঁটা।

'use client';
export default function Clock() {
  return <p>এখন: {Date.now()}</p>;
}
Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.

It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.

https://react.dev/link/hydration-mismatch

  …
                        <Page>
                          <Clock>
                            <p>
+                             1793640742674
-                             1793640742507
  …

বার্তাটি পড়তে শিখুন। প্রথম লাইন বলে কী মেলেনি (text) আর React কী করেছে: this tree will be regenerated on the client, মানে মেলেনি এমন অংশ client-এ নতুন করে বানানো হয়েছে। নিচের diff-এ + মানে client যা বানাল, - মানে server-এর HTML-এ যা ছিল। দুটি সময়ের পার্থক্য 167 ms: server-এর render আর ব্রাউজারের render-এর মাঝের সময়। Diff-এর ওপরের component-এর পথ (Page → Clock → p) বলে দেয় কোন ফাইল খুলবেন।

বাকি পাঁচটি পাতার diff-এর মূল অংশ এক টেবিলে। প্রতিটি সারি একই run-এর।

কারণকোডের মূল লাইনConsole-এ diff বা বার্তা
Math.random()className={'offer-' + off}+ className="offer-18"
- className="offer-27", সাথে text + 18 - 27
typeof windowtypeof window === 'undefined' ? 'server' : 'browser'+ browser
- server
Locale(1250).toLocaleString()+ ১,২৫০
- 1,250
Time zoneplacedAt.toLocaleString('bn-BD'), server TZ=UTC+ ৩/১১/২০২৬, ২:৩০:০০ AM
- ২/১১/২০২৬, ৮:৩০:০০ PM
HTML nesting<p>… <div>…</div></p>In HTML, %s cannot be a descendant of <%s>. This will cause a hydration error.%s <div> p, তারপর Hydration failed because the server rendered HTML didn't match the client.
Browser extensionকোডে কিছু নেইA tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. diff: - data-new-gr-c-s-check-loaded="14.1"

কয়েকটি সারি আলাদা করে বোঝা দরকার।

Locale. toLocaleString()-এ locale না দিলে চলমান পরিবেশের ডিফল্ট নেয়। আমাদের Node server-এর ডিফল্ট en-US, তাই 1,250; Edge-এর locale bn-BD, তাই ১,২৫০. একই কোড, দুই রকম অঙ্ক।

Time zone. এখানে locale দুই দিকেই bn-BD, বাংলা অঙ্কও দুই দিকে এসেছে। তবু মেলেনি, কারণ server-কে TZ=UTC দিয়ে চালিয়েছিলাম, যেমন অনেক cloud server চলে। একই মুহূর্ত (2026-11-02T20:30:00Z) UTC-তে ২ নভেম্বর রাত সাড়ে আটটা, ঢাকায় ৩ নভেম্বর রাত আড়াইটা। তারিখই বদলে গেছে। Bangla অঙ্ক নিয়ে বিস্তারিত পর্ব ৫৯-এ।

HTML nesting. <p>-এর ভেতরে <div> HTML-এর নিয়মে বৈধ নয়। Server যে HTML পাঠিয়েছে, তাতে লেখা ছিল <p>পণ্যের বিবরণ<div>…</div></p>. কিন্তু ব্রাউজারের HTML parser <div> দেখেই <p> বন্ধ করে দেয়, আর শেষের </p> থেকে একটি খালি <p> বানায়। DOM আর React-এর tree তাই আলাদা। প্রথম বার্তাটি dev-এর সতর্কতা; Playwright যেভাবে ধরেছে সেভাবেই দেখানো, যেখানে %s-এর জায়গার মানগুলো শেষে বসেছে। HTML-এর বিষয়বস্তুর নিয়ম HTML ও CSS কোর্সে আছে।

Browser extension. এখানে আমরা একটি সত্যিকারের extension চালাইনি। Playwright-এর init script দিয়ে React আসার আগেই <body>-এ একটি attribute বসিয়েছি, যেমন কিছু লেখা-সংশোধনের extension করে বলে জানা যায়। এবার বার্তা আলাদা: A tree hydrated, মানে hydration চলেছে, শুধু attribute মেলেনি, আর This won't be patched up, মানে React বাড়তি attribute সরাবে না। আপনার কোডে বাগ নেই, কিন্তু ব্যবহারকারীর ব্রাউজারে এই বার্তা আসবে।

Production-এ কী দেখায়

একই পাতা next build আর next start-এ চালালে বার্তা ছোট হয়ে যায়, কারণ production React-এ লম্বা বার্তা ফেলে দেওয়া থাকে:

Minified React error #418; visit https://react.dev/errors/418?args[]=text&args[]= for the full message or use the non-minified dev environment for full errors and additional helpful warnings.

আরেকটি চমক: build-এর route তালিকায় /date ছিল ○ (Static). মানে HTML build-এর সময়েই তৈরি। Server-এর HTML-এ সংখ্যাটি 1793641069503, ব্রাউজারে 1793641097552: পার্থক্য প্রায় ২৮ সেকেন্ড, কারণ build আর পাতা খোলার মাঝে ততটা সময় গেছে। আসল সাইটে এই পার্থক্য দিন বা মাস হতে পারে। Static পাতায় “এখন” মানে “build-এর সময়”, এটি পর্ব ৫৫-এর বিষয়ের সাথে জড়িত।

শিক্ষণ মডেল: React কী মেলায়

এই কোর্সের live React ব্লকে createRoot আছে, hydrateRoot export করা নেই। তাই নিচে একটি ছোট JavaScript মডেল: একই render function দুই পরিবেশে চলে, server-এর ফল HTML string হয়, আর “hydrate” সেই string-এর প্রতিটি node-এর লেখা client-এর লেখার সাথে মেলায়। আসল React element-এর ধরন, attribute আর আরও অনেক কিছু দেখে; এখানে শুধু লেখা।

// শিক্ষণ মডেল: server HTML আর client-এর প্রথম render মেলানো
function render(env) {
  return [
    { tag: 'h1', text: 'জামদানি শাড়ি' },
    { tag: 'p', text: 'দাম: ' + env.fmt(4500) },
    { tag: 'p', text: 'বানিয়েছে: ' + env.where },
  ];
}
function toHtml(tree) {
  return tree.map(n => `<${n.tag}>${n.text}</${n.tag}>`).join('');
}
function hydrate(html, tree) {
  const dom = [...html.matchAll(/<(\w+)>(.*?)<\/\1>/g)]
    .map(m => ({ tag: m[1], text: m[2] }));
  for (let i = 0; i < tree.length; i++) {
    if (dom[i].text !== tree[i].text) {
      console.log(`  node ${i} <${tree[i].tag}> mismatch`);
      console.log('  + client: ' + tree[i].text);
      console.log('  - server: ' + dom[i].text);
      return 'tree regenerated on client';
    }
    console.log(`  node ${i} <${tree[i].tag}> ok`);
  }
  return 'hydrated, handlers attached';
}
const en = n => n.toLocaleString('en-US');
const bn = n => n.toLocaleString('bn-BD');
console.log('১) server-এ en-US, browser-এ bn-BD:');
const html1 = toHtml(render({ fmt: en, where: 'server' }));
console.log('  →', hydrate(html1, render({ fmt: bn, where: 'browser' })));
console.log('২) দুই দিকে একই locale, একই লেখা:');
const html2 = toHtml(render({ fmt: bn, where: 'বাজারঘর' }));
console.log('  →', hydrate(html2, render({ fmt: bn, where: 'বাজারঘর' })));

আউটপুট:

১) server-এ en-US, browser-এ bn-BD:
  node 0 <h1> ok
  node 1 <p> mismatch
  + client: দাম: ৪,৫০০
  - server: দাম: 4,500
  → tree regenerated on client
২) দুই দিকে একই locale, একই লেখা:
  node 0 <h1> ok
  node 1 <p> ok
  node 2 <p> ok
  → hydrated, handlers attached

প্রথম ক্ষেত্রে মডেল node 1-এ থেমেছে: প্রথম অমিলই যথেষ্ট। node 2-এর server/browser অমিল আর দেখাই হয়নি। আসল React-এও diff সাধারণত প্রথম সমস্যার জায়গা দেখায়, তাই একটি ঠিক করার পর আরেকটি বের হতে পারে। দ্বিতীয় ক্ষেত্রে locale স্পষ্ট করে দেওয়া হয়েছে আর পরিবেশ-নির্ভর লেখা সরানো হয়েছে, তাই তিনটি node-ই মিলেছে।

৪ · সমাধান: আগে একই জিনিস, তারপর client-এর জন্য আলাদা ধাপ

দর্জির কথা ভাবুন। দোকানে মাপ নিয়ে কাপড় কাটা হলো (server), বাড়িতে গিয়ে আরেকজন একই মাপের খাতা দেখে সেলাই করবেন (client), এমন ব্যবস্থা। খাতায় যদি “আজকের তারিখ অনুযায়ী লম্বা” লেখা থাকে, দুই দিন দুই রকম ফল আসবে। ভালো খাতায় মাপ নির্দিষ্ট সংখ্যায় লেখা থাকে। Mismatch-এর সমাধানও সেই ক্রমে ভাবুন।

১. একই জিনিস render করুন

Locale আর time zone স্পষ্ট দিন: toLocaleString('bn-BD', { timeZone: 'Asia/Dhaka' }). Random মান server-এ একবার বানিয়ে props হিসেবে পাঠান। HTML nesting ঠিক করুন। এটিই সবচেয়ে সস্তা আর সঠিক পথ।

প্রথম পছন্দ

২. Client-এর মান effect-এ

যে মান শুধু ব্রাউজারে আছে (পর্দার মাপ, localStorage), সেটি প্রথম render-এ নয়, useEffect-এ পড়ুন। একে বলে two-pass rendering.

দাম: একটি বাড়তি render

৩. এক element-এ নীরবতা

suppressHydrationWarning শুধু সেই element-এর নিজের লেখা বা attribute-এর জন্য, এক স্তর গভীর পর্যন্ত। React লেখা ঠিকও করবে না।

শেষ উপায়

৪. পুরো অংশ client-এ

dynamic(() => import(...), { ssr: false }) সেই component-কে server-এ render-ই করে না। HTML-এ শুধু loading লেখা যায়।

দাম: খালি জায়গা, পরে লোড

চারটি পথ একটি পাতায় একসাথে চালালাম। সত্যিকারের run, Next.js 16.4.4. পাতার client component:

'use client';
import { useEffect, useState, useSyncExternalStore } from 'react';
import dynamic from 'next/dynamic';

const MapWidget = dynamic(() => import('./MapWidget'), {
  ssr: false,
  loading: () => <p>ম্যাপ লোড হচ্ছে…</p>,
});

function subscribe(cb) {
  window.addEventListener('online', cb);
  window.addEventListener('offline', cb);
  return () => {
    window.removeEventListener('online', cb);
    window.removeEventListener('offline', cb);
  };
}

export default function Fixes() {
  const [now, setNow] = useState(null);
  useEffect(() => { setNow(Date.now()); }, []);
  const online = useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
  return (
    <div>
      <p>এখন: {now === null ? '—' : now}</p>
      <p suppressHydrationWarning>রেন্ডারের সময়: {Date.now()}</p>
      <p>নেটওয়ার্ক: {online ? 'অনলাইন' : 'অফলাইন'}</p>
      <MapWidget />
    </div>
  );
}

MapWidget সরাসরি window.innerWidth পড়ে, যা server-এ নেই। Production build-এর HTML থেকে <body>-এর অংশ (script বাদে):

<div><p>এখন: <!-- -->—</p><p>রেন্ডারের সময়: <!-- -->1793641069482</p><p>নেটওয়ার্ক: <!-- -->অনলাইন</p><!--$!--><template data-dgst=""></template><p>ম্যাপ লোড হচ্ছে…</p><!--/$--></div>

আর hydration-এর পরে পর্দায়, কোনো error ছাড়া:

এখন: 1793640766588
রেন্ডারের সময়: 1793640766589
নেটওয়ার্ক: অনলাইন
ম্যাপ: পর্দার প্রস্থ 1280px

চারটি লাইন, চারটি শিক্ষা। প্রথম লাইনে server আর hydration দুই জায়গাতেই —, তাই মিলেছে; effect পরে আসল সময় বসিয়েছে। দ্বিতীয় লাইনে suppressHydrationWarning warning চুপ করিয়েছে। খেয়াল করুন, dev run-এ এই লাইনে পর্দায় নতুন সময় এসেছে, কারণ একই component-এর প্রথম লাইনের effect-এর state বদলে পুরো component আবার render হয়েছে। Docs-এর ভাষায় React অমিল লেখা ঠিক করার চেষ্টা করবে না; একা থাকলে পুরোনো লেখাই থেকে যেতে পারে। তৃতীয় লাইন useSyncExternalStore-এর, পরের অংশে। চতুর্থটি ssr: false: HTML-এ <!--$!--> চিহ্ন মানে এই Suspense boundary server-এ বাদ দিয়ে client-এ render করতে বলা হয়েছে, আর জায়গায় loading লেখা।

ssr: false-এর দাম। HTML-এ ওই অংশ নেই, তাই search engine দেখবে না, ধীর ফোনে খালি জায়গা থাকবে, আর আলাদা chunk নামার পর তবেই দেখা দেবে। Next.js-এর docs বলে, এই option শুধু client component-এ কাজ করে; server component-এ লিখলে error আসে। তাই আমাদের ফাইলে 'use client' আছে। নীতি: পুরো ম্যাপ, chart বা editor-এর মতো সত্যিকারের ব্রাউজার-নির্ভর বড় অংশে ব্যবহার করুন, একটি তারিখ ঠিক করতে নয়।

Two-pass rendering নিজে চালিয়ে দেখুন

নিচের ব্লকে React 19 চলে, তবে createRoot দিয়ে, তাই এটি hydration নয়। এখানে দেখার বিষয় শুধু render-এর ক্রম: প্রথম render-এ client-only মান নেই, effect চলার পরে দ্বিতীয় render-এ আসে। hydrateRoot-এর সাথে ঠিক এই ক্রমেই প্রথম render server-এর HTML-এর সাথে মেলে।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
let renders = 0;
function ScreenBadge() {
  // server-এ window নেই, তাই প্রথম render-এ null
  const [size, setSize] = useState(null);
  renders++;
  console.log(`render ${renders}: size = ${size === null ? 'null' : 'মাপা হয়েছে'}`);
  useEffect(() => {
    console.log('effect: এখন browser-এ, window পড়া নিরাপদ');
    setSize(window.innerWidth >= 600 ? 'বড় পর্দা' : 'ছোট পর্দা');
  }, []);
  return <p>{size === null ? 'পর্দা মাপা হচ্ছে…' : size}</p>;
}
createRoot(document.getElementById('root')).render(<ScreenBadge />);
</script>

আউটপুট:

render 1: size = null
effect: এখন browser-এ, window পড়া নিরাপদ
render 2: size = মাপা হয়েছে

দুটি render, একটি effect. প্রথম render-এর লেখা “পর্দা মাপা হচ্ছে…”, যা server-ও বানাতে পারে। react.dev সতর্ক করে: এই পথে hydration ধীর হয়, কারণ component দুবার render হয়, আর ধীর সংযোগে ব্যবহারকারী লেখাটির বদল দেখতে পান। তাই এটি দ্বিতীয় পছন্দ, প্রথম নয়। Effect-এর জীবনচক্র পর্ব ১৩-এ।

1'use client';2export default function Price() {3  const text = (1250).toLocaleString();4  return <p>দাম: {text}</p>;5}6// সমাধান: toLocaleString('bn-BD')
দুই দিকের অবস্থা
কোথায় চলছে
Server HTML
Client render
পর্দায়
React-এর সিদ্ধান্ত
  1. Server-এ render. Locale দেওয়া নেই, তাই Node-এর ডিফল্ট en-US নিল। HTML-এ গেল “দাম: 1,250”.
  2. HTML আঁকা। ব্রাউজার JavaScript ছাড়াই “দাম: 1,250” দেখাল। এখন পর্যন্ত কেউ জানে না যে সমস্যা আছে।
  3. Hydration-এর render. একই লাইন এবার Edge-এ চলল, যার locale bn-BD. Client-এর tree-তে “দাম: ১,২৫০”. React DOM-এর লেখার সাথে মেলাচ্ছে।
  4. অমিল। লেখা আলাদা। React onRecoverableError-এ error পাঠাল, আর এই অংশ client-এ নতুন করে বানাল। পর্দায় অঙ্ক হঠাৎ বদলে বাংলা হলো। কাজ দুবার হলো।
  5. সমাধান। লাইন ৩-এ toLocaleString('bn-BD') লিখলে server আর client দুই দিকেই “দাম: ১,২৫০”. মিলেছে, handler জুড়েছে, error নেই। তারিখ হলে timeZone-ও দিন।

৫ · useSyncExternalStore আর getServerSnapshot

React-এর বাইরের কোনো উৎস থেকে মান পড়তে পর্ব ১৮-এ useSyncExternalStore দেখেছেন। এর তৃতীয় argument getServerSnapshot ঠিক hydration-এর জন্য বানানো। react.dev অনুযায়ী এটি চলে মাত্র দুই জায়গায়: server-এ HTML বানানোর সময়, আর client-এ hydration-এর সময়। Hydration শেষ হলে React getSnapshot-এর আসল মানে চলে যায়, আর দরকার হলে আবার render করে।

export function useOnline() {
  return useSyncExternalStore(
    subscribe,                 // online/offline ইভেন্টে কান পাতা
    () => navigator.onLine,    // ব্রাউজারে আসল মান
    () => true,                // server আর hydration-এ নির্দিষ্ট মান
  );
}

নিয়ম দুটি। এক, getServerSnapshot server-এ যা দিয়েছিল, hydration-এ client-এও হুবহু তাই দিতে হবে। Store-এ server থেকে আসা ডেটা থাকলে সেটি HTML-এর সাথে পাঠাতে হয়, যাতে client-এ একই মান পাওয়া যায়। দুই, argument-টি বাদ দিলে server-এ render করতে গেলে error হয়। Docs বলে, server-এ অর্থপূর্ণ মান না থাকলে বাদ দিয়ে সেই অংশকে client-এ render হতে দেওয়াই পথ।

useEffect-এর পথের সাথে তফাত কী? ফল প্রায় এক: প্রথমে নির্দিষ্ট মান, পরে আসল মান। কিন্তু useSyncExternalStore পরের বদলগুলোতেও (নেট চলে গেলে) সঠিকভাবে সাড়া দেয়, আর একই store অনেক component পড়লে সবাই একই মান দেখে। Zustand-এর মতো library ভেতরে এটিই ব্যবহার করে।

লেখার সময়ে একটি নতুন দিক: react.dev-এর hydrateRoot পাতায় client-only component-এর জন্য use(browser()) নামে একটি পথের উল্লেখ আছে। আমাদের যাচাইয়ে stable react 19.3.1 package-এ browser নামে কোনো export পাওয়া যায়নি, তাই এটি সম্ভবত পরের version-এর। ব্যবহারের আগে আপনার version-এর release notes দেখুন।

৬ · Selective hydration: Suspense আর hydration-এর ক্রম

পুরো পাতা এক টানে hydrate হলে বড় পাতায় long task লম্বা হয়। React 18 থেকে Suspense boundary hydration-এর ভাগও ঠিক করে। প্রতিটি boundary আলাদা টুকরো হিসেবে hydrate হতে পারে, আর streaming-এ যে boundary-র HTML পরে আসে, সেটি পরে hydrate হয়।

প্রথম পরীক্ষা: একটি পাতায় হেডারের বোতাম, আর দুটি Suspense boundary. ভেতরের server component connection() ডেকে পাতাকে dynamic করে, তারপর একটি 800 ms, অন্যটি 1600 ms অপেক্ষা করে। Production build-এর route তালিকায় পাতাটি ƒ /suspense. Server-এর প্রথম HTML টুকরো:

<main><button>header<!-- --> (<!-- -->0<!-- -->)</button><!--$?--><template id="B:0"></template><p>রিভিউ লোড হচ্ছে…</p><!--/$--><!--$?--><template id="B:1"></template><p>পণ্য লোড হচ্ছে…</p><!--/$--></main>
…
<div hidden id="S:1"><section><p>মিল আছে এমন পণ্য</p><button>related…</button></section></div><script>$RC("B:1","S:1")</script>

<!--$?--> মানে অপেক্ষমাণ boundary, ভেতরে fallback. পরে একই সংযোগে লুকানো <div hidden id="S:1">-এ আসল HTML আসে, আর ছোট $RC script fallback-এর জায়গায় সেটি বসায়। প্রতিটি client component-এর useEffect সময় লিখে রাখে। ব্রাউজারের console, সত্যিকারের run:

hydrated: header 98ms
hydrated: reviews 835ms
hydrated: related 1636ms

হেডার আগে সচল, রিভিউ বা অন্য পণ্যের জন্য অপেক্ষা করেনি। প্রতিটি অংশ তার HTML আসার পরপরই hydrate হয়েছে।

ক্লিক অগ্রাধিকার পায়

দ্বিতীয় পরীক্ষা আরও আকর্ষণীয়। দুটি boundary-ই প্রথম HTML-এ আছে, কিন্তু প্রতিটির ভেতরে ২০টি ধীর component, প্রত্যেকটি render-এ ইচ্ছা করে 15 ms ব্যস্ত থাকে। মানে প্রতিটি boundary hydrate করতে প্রায় 300 ms. Boundary দুটি পাতায় এই ক্রমে: reviews তারপর related. দুটি run: একবার কিছু না করে, একবার JavaScript আসার সাথে সাথে related-এর “কিনুন” বোতামে ক্লিক।

$ python probe_selective.py
--- no click
hydrated: reviews 710ms
hydrated: related 710ms
--- with early click on related
user clicked related at ~271ms
hydrated: related 664ms
click handled: related 666ms
hydrated: reviews 969ms

ক্লিক না করলে React পাতার ক্রমে কাজ করেছে, আর দুটির effect একসাথে চলেছে। ক্লিক করলে ক্রম উল্টে গেল: related আগে hydrate হলো, আর 2 ms পরে সেই পুরোনো ক্লিকের handler চলল। ক্লিকটি হারায়নি, React সেটি ধরে রেখে পরে আবার চালিয়েছে, একে বলে event replay. তারপর প্রায় 305 ms পরে reviews, যা প্রায় ২০ × 15 ms-এর সমান। একবার চালানোয় দ্বিতীয় run-এর সংখ্যা ছিল 649, 651 আর 955 ms; ক্রম একই।

  1. HTML2 boundaryসব দেখা যায়
  2. JS চললhydrateRootroot-এ listener
  3. ক্লিকrelated~271 ms
  4. আগে relatedreplay664 ms
  5. তারপর reviewshydrated969 ms
Boundary-র অবস্থা (প্রথম run)
সময়
reviews
related
ক্লিকের অবস্থা
  1. HTML. দুটি boundary-র পুরো HTML পর্দায়: reviews আগে, related পরে। কোনোটিতে handler নেই।
  2. JS চলল। Next.js hydrateRoot ডাকল। React root-এ event listener বসায়, তাই এখন থেকে ক্লিক React-এর কানে পৌঁছায়, যদিও boundary-গুলো তখনো hydrate হয়নি।
  3. ক্লিক। ব্যবহারকারী প্রায় 271 ms-এ related-এর বোতাম চাপলেন। সেই boundary এখনো hydrate হয়নি। React ক্লিকটি ধরে রাখল আর এই boundary-কে অগ্রাধিকার দিল।
  4. আগে related. 664 ms-এ related hydrate শেষ, 666 ms-এ জমা ক্লিকের handler চলল। পাতার ক্রম ভেঙে ব্যবহারকারীর ইচ্ছা আগে এল।
  5. তারপর reviews. 969 ms-এ বাকি boundary. ক্লিক না করলে দুটিই প্রায় 710 ms-এ একসাথে হতো; ক্লিক মোট সময় বাড়ায়নি, কাজের ক্রম বদলেছে।

সীমা মনে রাখুন: Replay কাজ করে কারণ React-এর JavaScript ততক্ষণে চলছে আর root-এ listener বসেছে। অংশ ২-এর হারানো ক্লিকগুলো ছিল তার আগের, যখন JavaScript নামছিল। তখন কেউ ক্লিক ধরে রাখে না। তাই selective hydration মৃত সময় ছোট করে, শূন্য করে না। আর পাতার সব কিছু একটি boundary-তে থাকলে ভাগ করার কিছুই নেই।

৭ · বড় পাতার খরচ আর server component

Hydration-এর খরচ তিন ভাগে: JavaScript নামানো, সেটি parse করে চালানো, আর tree-র প্রতিটি element DOM-এর সাথে মেলানো। পর্ব ৫২-তে দেখবেন, server component-এর কোড ব্রাউজারে যায় না। তাহলে কি server component ব্যবহার করলে hydration কমে যায়? মেপে দেখি।

একই পণ্যের পাতা দুইভাবে বানালাম। /product: পাতা server component, শুধু “কার্টে যোগ” একটি ছোট client component. /product-all: পুরো পাতা একটি client component, ৬০০টি রিভিউ JSON import করে। দুটিই ○ (Static). Build-এর ফাইলের মাপ আর একই শর্তের Edge run (CPU ৬ গুণ, ধীর নেটওয়ার্ক, পাঁচবারের মধ্যমা):

মাপServer + ছোট clientসব client
HTML ফাইল (gzip-এ)249,956 bytes (প্রায় 16.7 kB)105,650 bytes (প্রায় 6.5 kB)
RSC payload ফাইল131,264 bytes3,427 bytes
JS, decoded457,458 bytes550,752 bytes
JS, transfer137,277 bytes143,185 bytes
FCP520 ms548 ms
Hydrated (effect)1360 ms1398 ms
প্রথম সফল ক্লিক1418 ms1484 ms

ফলটি অনেকের প্রত্যাশার চেয়ে ছোট, তাই সৎভাবে পড়ুন। Server component-এর পাতায় 93,682 bytes-এর একটি JS chunk কমেছে, যাতে রিভিউর ডেটা আর component ছিল। কিন্তু gzip-এর পরে পার্থক্য মাত্র প্রায় 6 kB, আর HTML উল্টো বড় হয়েছে। কারণ RSC payload: server component-এর ফল একটি বিশেষ বিন্যাসে HTML-এর ভেতরেই script হিসেবে আসে, যাতে client-এ React পুরো tree জানে। তাই ৬০০টি রিভিউ দুবার এসেছে: একবার HTML হিসেবে, একবার payload হিসেবে। আমরা গুনেছি, “রহিম” নামটি server পাতার HTML ফাইলে ১০০ বার, অন্যটিতে ৫০ বার।

সময়ের পার্থক্য এই run-এ মাত্র 40 থেকে 70 ms. কেন এত কম? দুই পাতাতেই প্রায় 450 kB-র framework JavaScript আছে, যা মোট খরচের বড় অংশ। আর App Router-এ client-এ React পুরো tree ধরে, server component-এর বানানো element-গুলোও DOM-এর সাথে মেলায়। Server component বাঁচায় সেই component-এর কোড নামানো আর চালানোর খরচ, element মেলানোর কাজ পুরো নয়।

কখন বড় লাভ

Server component-এর লাভ বড় হয় যখন client component ভারী library টানে: markdown parser, তারিখের library, syntax highlighter, বড় icon set. সেগুলো server-এ থাকলে ব্রাউজারে একটি byte-ও যায় না। আমাদের পরীক্ষায় শুধু ডেটা ছিল, ভারী কোড ছিল না, তাই লাভ ছোট। নিজের পাতায় ঠিক এই মাপটি নিন; অনুমান করবেন না।

৮ · অন্য পথ: islands আর resumability

সব framework hydration-কে একইভাবে দেখে না। দুটি বিকল্প ধারণা জানা থাকা দরকার, কারণ ইন্টারভিউতে প্রায়ই তুলনা আসে। নিচের তথ্য এই পর্বে চালিয়ে যাচাই করা হয়নি; ধারণা হিসেবে পড়ুন, আর ব্যবহারের আগে নিজ নিজ docs দেখুন।

Islands. পাতার বেশিরভাগ অংশ স্থির HTML, যাতে কোনো JavaScript জোড়া হয় না। শুধু ছোট ছোট interactive “দ্বীপ” আলাদাভাবে hydrate হয়। নামটি ২০১৯ সালে Etsy-র Katie Sylor-Miller-এর ধারণা থেকে আসে বলে জানা যায়, আর ২০২০ সালে Jason Miller-এর একটি লেখায় জনপ্রিয় হয়। Astro (1.0 এসেছে ২০২২ সালে) এই পথের পরিচিত উদাহরণ: component-এ client:load, client:idle বা client:visible-এর মতো নির্দেশ দিলে তবেই সেটি hydrate হয়। রিভিউর মতো স্থির তালিকা তখন hydration-এর খরচে ঢোকেই না। দাম: দ্বীপগুলো একে অন্যের state সহজে ভাগ করতে পারে না, আর পুরো অ্যাপের মতো client-side navigation-এর জন্য বাড়তি কাজ লাগে।

Resumability. Qwik (Angular-এর স্রষ্টাদের একজন Miško Hevery-র নেতৃত্বে, 1.0 এসেছে ২০২৩ সালে) আরও এক ধাপ যায়। Server তার অবস্থা আর কোন event-এ কোন handler, সেই তথ্য HTML-এ লিখে দেয়। ব্রাউজার শুরুতে component আবার চালায় না; ক্লিক হলে তবেই সেই handler-এর ছোট কোড নামিয়ে চালায়, যেন server যেখানে থেমেছিল সেখান থেকে “আবার শুরু”। দাম: প্রথম ক্লিকে কোড নামানোর দেরি হতে পারে, যা এই framework prefetch দিয়ে কমানোর চেষ্টা করে, আর চিন্তার ধরন React থেকে আলাদা।

পথব্রাউজারে শুরুতে কী চলেভালো যেখানেখরচ যেখানে
পুরো hydration (React, Next.js)পুরো tree মেলানোঅনেক interactive, অ্যাপের মতো পাতাবড় পাতায় JS আর long task
Islands (যেমন Astro)শুধু দ্বীপগুলোলেখা-প্রধান পাতা, ব্লগ, পণ্যের বিবরণদ্বীপের মধ্যে state ভাগ
Resumability (যেমন Qwik)প্রায় কিছুই নাখুব ধীর ফোনে প্রথম লোডপ্রথম ক্লিকে কোড আনা, ভিন্ন মডেল

সম্প্রদায় এখানে একমত নয়। একদল বলে, React-এর server component আর selective hydration মিলে islands-এর বেশিরভাগ লাভ দেয়, আর পূর্ণ অ্যাপের সুবিধা রাখে। অন্যদল বলে, যতক্ষণ পুরো tree মেলাতে হয়, ততক্ষণ বড় পাতায় খরচ থেকেই যায়। আমাদের মাপে দ্বিতীয় দলের কথায় কিছু সত্য আছে। সিদ্ধান্ত নিন পাতার ধরন দেখে: “বাজারঘর”-এর কার্ট আর চেকআউট অ্যাপের মতো, ব্লগ আর সাহায্যের পাতা লেখা-প্রধান।

৯ · বাজারঘর-এর পণ্যের পাতা: একটি hydration বাজেট

পর্ব ২৩-এ performance budget-এর ধারণা পেয়েছেন। এখানে hydration-এর জন্য একটি সরল হিসাব। মূল সূত্র:

interactive ≈ paint + JS নামানো + (decoded JS × চালানোর খরচ) + (element × মেলানোর খরচ)
মৃত সময় = interactive − paint

ধরুন নেটওয়ার্ক 200 kB/s (প্রায় 1.6 Mbps), RTT 150 ms. Paint-এর আগে সংযোগ, অনুরোধ আর HTML মিলিয়ে ৩টি RTT. সস্তা ফোনে decoded JavaScript-এর প্রতি kB-তে 1.0 ms, আর প্রতি element মেলাতে 0.1 ms. এই সংখ্যাগুলো অনুমান, শেখার জন্য বেছে নেওয়া; নিজের লক্ষ্য-ফোনে মেপে বদলে নিন। মডেলটি সরলও: বাস্তবে JavaScript HTML-এর সাথে সমান্তরালে নামতে শুরু করে। বাজেট: interactive ≤ 2500 ms, মৃত সময় ≤ 1500 ms. KB-র সংখ্যা ওপরের build থেকে।

// বাজারঘর পণ্যের পাতা: hydration বাজেটের সরল হিসাব
// KB-র সংখ্যা একটি local build থেকে; নেটওয়ার্ক আর CPU ধরে নেওয়া
const net = { kBps: 200, rtt: 150 };        // ধীর মোবাইল সংযোগ, অনুমান
const cpu = { msPerKB: 1.0, msPerEl: 0.1 }; // সস্তা ফোন, অনুমান
const budget = { interactive: 2500, dead: 1500 };
const plans = [
  { name: 'সব client component', html: 6.7, js: 143, jsRaw: 551, els: 1205 },
  { name: 'server + ছোট client', html: 16.8, js: 137, jsRaw: 457, els: 1205 },
  { name: 'server, ২০টি রিভিউ', html: 2.5, js: 137, jsRaw: 457, els: 45 },
  { name: 'islands (ধরে নেওয়া)', html: 6.7, js: 8, jsRaw: 20, els: 1 },
];
const down = kb => (kb / net.kBps) * 1000;
for (const p of plans) {
  const paint = 3 * net.rtt + down(p.html);   // সংযোগ, অনুরোধ, HTML
  const jsReady = paint + net.rtt + down(p.js);
  const run = p.jsRaw * cpu.msPerKB;           // parse আর চালানো
  const hyd = p.els * cpu.msPerEl;             // DOM মিলিয়ে handler জোড়া
  const ready = jsReady + run + hyd;
  const dead = ready - paint;
  const ok = ready <= budget.interactive && dead <= budget.dead;
  console.log(`${p.name}\n  paint ${paint.toFixed(0)}  JS ${jsReady.toFixed(0)}` +
    `  চালানো +${run.toFixed(0)}  hydrate +${hyd.toFixed(0)}` +
    `\n  interactive ${ready.toFixed(0)} ms, মৃত সময় ${dead.toFixed(0)} ms → ${ok ? 'বাজেটে' : 'বাজেটের বাইরে'}`);
}

আউটপুট:

সব client component
  paint 484  JS 1349  চালানো +551  hydrate +121
  interactive 2020 ms, মৃত সময় 1537 ms → বাজেটের বাইরে
server + ছোট client
  paint 534  JS 1369  চালানো +457  hydrate +121
  interactive 1947 ms, মৃত সময় 1413 ms → বাজেটে
server, ২০টি রিভিউ
  paint 463  JS 1298  চালানো +457  hydrate +5
  interactive 1759 ms, মৃত সময় 1297 ms → বাজেটে
islands (ধরে নেওয়া)
  paint 484  JS 674  চালানো +20  hydrate +0
  interactive 694 ms, মৃত সময় 210 ms → বাজেটে

প্রথম সারি হাতে যাচাই করুন। Paint: 3 × 150 = 450, HTML 6.7 ÷ 200 × 1000 = 33.5, মোট 483.5 ≈ 484 ms. JS: 150 + 143 ÷ 200 × 1000 = 865, তাই 1348.5 ms. চালানো 551 × 1.0 = 551, মেলানো 1205 × 0.1 = 120.5. যোগফল 2020 ms; মৃত সময় 2020 − 483.5 ≈ 1537 ms, সীমার 37 ms বাইরে। দ্বিতীয় পরিকল্পনা ঠিক ঢুকেছে, কিন্তু খুব কম ফাঁকে। তৃতীয়টি দেখায়, রিভিউ ২০টিতে নামালে মেলানোর খরচ প্রায় শূন্য, কিন্তু মোট সময়ে লাভ শুধু প্রায় 190 ms. বাকি সময় ঠিক করে framework-এর JavaScript, যা প্রতিটি React পরিকল্পনায় একই। Islands-এর সংখ্যা ধরে নেওয়া, মাপা নয়।

এই হিসাব থেকে “বাজারঘর”-এর পণ্যের পাতার সিদ্ধান্ত:

আরও গভীরে: React DOM-এর hydration-এ প্রতিটি fiber “বর্তমান DOM node”-এর একটি pointer ধরে এগোয়, আর অমিল পেলে নিকটতম Suspense boundary পর্যন্ত পিছিয়ে সেখান থেকে client render করে; boundary না থাকলে root পর্যন্ত। তাই Suspense শুধু loading-এর জন্য নয়, mismatch-এর ক্ষতিকে ছোট রাখার দেয়ালও। Streaming-এর <!--$?-->, <!--$!--> চিহ্নগুলো React-এর ভেতরের বিন্যাস, যা version-এর সাথে বদলাতে পারে; এর ওপর কোড লিখবেন না। বিষয়টির মূল আলোচনা React 18-এর working group-এ “New Suspense SSR Architecture” লেখায় (২০২১) পাওয়া যায়।

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

  • Hydration কী? Server-এর HTML-এ client-এর React tree মিলিয়ে event handler জুড়ে পাতা সচল করা।
  • Hydration mismatch-এর তিনটি কারণ? Date.now() বা random মান, typeof window-এর শাখা, locale বা time zone-এর পার্থক্য; এ ছাড়া ভুল HTML nesting আর extension.
  • suppressHydrationWarning কি সমস্যা ঠিক করে? না; এক element-এর এক স্তরের warning চুপ করায়, লেখা ঠিক করে না।
  • Selective hydration কী? Suspense boundary আলাদাভাবে hydrate হয়, আর ব্যবহারকারী যেটিতে ক্লিক করেন সেটি অগ্রাধিকার পায়।
  • Islands আর resumability-র তফাত? Islands শুধু নির্বাচিত অংশ hydrate করে; resumability শুরুতে প্রায় কিছুই আবার চালায় না, ক্লিকে handler আনে।

১০ · AI-এর যুগে: hydration error AI-কে দেখানোর আগে

Hydration error-এ AI-এর সবচেয়ে প্রিয় উত্তর দুটি: “suppressHydrationWarning দিন” আর “ssr: false দিন”। দুটিই error সরায়, কারণ সরায় না। একটি চোখ বন্ধ করে, অন্যটি পাতার অংশ server থেকে সরিয়ে দেয়। একটি মজার বাস্তব তথ্য: আমাদের next dev run-এ Next.js 16.4.4 নিজেই প্রজেক্টে একটি AGENTS.md ফাইল লিখেছে, যার প্রথম শিরোনাম “This is NOT the Next.js you know”, আর বলে কোড লেখার আগে node_modules/next/dist/docs/-এর guide পড়তে। Framework নিজেই জানে, AI পুরোনো version মনে রাখে।

AI যা প্রায়ই করেসমস্যাযাচাই
Parent <div>-এ suppressHydrationWarningএক স্তরের বেশি কাজ করে না, আর লেখা ঠিক হয় নাDev-এ আবার চালিয়ে console দেখুন
সব জায়গায় ssr: falseSEO আর প্রথম দেখা নষ্ট, server component-এ errorBuild-এর HTML-এ লেখাটি আছে কি না খুঁজুন
if (typeof window !== 'undefined') দিয়ে render-এ শাখাঠিক এটিই mismatch-এর কারণuseEffect বা useSyncExternalStore-এ সরান
পুরোনো ReactDOM.hydrate বা pages router-এর getServerSidePropsReact 18-এ বদলেছে, App Router-এ নেইনিজের version-এর react.dev আর nextjs.org পড়ুন
Locale ছাড়া toLocaleString()Server আর ব্রাউজারে আলাদা ফলbn-BD আর timeZone স্পষ্ট লিখুন
Client component-এ server-এর secret দিয়ে ডেটা আনাSecret bundle-এ চলে যায়Build-এর JS-এ secret খুঁজুন; পর্ব ৭৮ দেখুন

AI-কে error-এর পুরো diff দিন, আর সমাধানের আগে কারণ চান।

আমি Next.js 16.4 (App Router) আর React 19.3 ব্যবহার করি। dev-এ এই hydration error এসেছে: [পুরো error আর + / - diff] [যে component-এর নাম diff-এ আছে, তার পুরো কোড] ১. diff দেখে বলো server আর client-এ ঠিক কোন মান আলাদা, আর কেন। ২. প্রথমে এমন সমাধান দাও যাতে দুই দিকে একই জিনিস render হয়। তা সম্ভব না হলে কারণ বলো। ৩. suppressHydrationWarning বা ssr: false দিও না, যদি না বিকল্প নেই বলে প্রমাণ করতে পারো। ৪. প্রতিটি API-র জন্য react.dev বা nextjs.org-এর পাতা বলো। নতুন package যোগ করো না।

AI-এর hydration সমাধান নেওয়ার আগে

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Server তো পুরো পাতাই বানিয়েছে। তাহলে ব্রাউজারে আবার একই component চালানোর দরকার কী? এটি কি অপচয় নয়?

প্রশ্নটি বহু বছর ধরে ওয়েব প্রকৌশলে ঘুরছে। Rails, Django বা PHP-র পুরোনো যুগে server HTML পাঠাত, আর ব্রাউজারে jQuery-র মতো ছোট script নির্দিষ্ট element খুঁজে handler জুড়ত। কোনো “আবার render” ছিল না। সমস্যা ছিল দুই জায়গায় দুই রকম কোড: চেহারা server-এর template-এ, আচরণ আলাদা JavaScript-এ। বড় হলে দুটি মিল রাখা কঠিন হতো।

React একটি component মডেল আনে: চেহারা আর আচরণ এক জায়গায়, একই কোড। ২০১৩ সালের পরে SPA জনপ্রিয় হলো, কিন্তু প্রথম লোড ধীর আর search engine-এর জন্য খালি পাতা। তাই একই component server-এও চালানো শুরু হলো। এখন component একবার লেখা, দুই জায়গায় চলে। কিন্তু ব্রাউজারের React জানে না কোন DOM node কোন component-এর, কোন handler কোথায় যাবে, state কী। এই জ্ঞান ফেরত পেতেই সে component আবার চালায়। HTML-এ function পাঠানো যায় না, আর DOM দেখে component-এর ভেতরের state বোঝা যায় না।

তাই হ্যাঁ, এখানে দ্বিগুণ কাজ আছে, আর একে অনেকে “hydration tax” বলেন। আমাদের মাপে প্রায় 450 kB decoded JavaScript আর দুটি long task এই করের অংশ। কিন্তু বিনিময়ে একটি কোডবেস, একটি মানসিক মডেল, আর পাতা থেকে পাতায় অ্যাপের মতো চলাচল পাওয়া যায়। কর কমানোর চেষ্টাই এই পর্বের বাকি অংশ: server component (কিছু component ব্রাউজারে না পাঠানো), selective hydration (ভাগ করে, প্রয়োজন অনুযায়ী), islands (স্থির অংশ বাদ), আর resumability (আবার না চালিয়ে server-এর অবস্থা থেকে শুরু)।

লেখা-প্রধান পাতার জন্য পুরোনো পথ এখনো ভালো হতে পারে, এ নিয়ে সম্প্রদায়ে বিতর্ক আছে। htmx-এর মতো টুলের জনপ্রিয়তা দেখায়, সবাই পুরো client tree চান না।

মূল উপলব্ধি: Hydration হলো “এক কোড, দুই জায়গা” মডেলের দাম। প্রশ্ন অপচয় আছে কি না নয়, প্রশ্ন আপনার পাতা এই দামের বিনিময়ে যথেষ্ট পাচ্ছে কি না।

প্র ০২React mismatch পেলে লেখা ঠিক করে দেয় না কেন? DOM তো সামনেই আছে, বদলে দিলেই হয়।

এর পেছনে performance আর নিরাপত্তা দুই কারণই আছে। react.dev স্পষ্ট বলে: attribute-এর পার্থক্য ঠিক করার কোনো নিশ্চয়তা নেই, কারণ বেশিরভাগ অ্যাপে mismatch বিরল, আর সব markup যাচাই করা খুব ব্যয়বহুল। Hydration-এর পুরো উদ্দেশ্য দ্রুত হওয়া: DOM আছে, তাই নতুন বানানো নয়, শুধু জোড়া। প্রতিটি attribute-এর তুলনা আর সংশোধন করলে সেই লাভ কমে।

React-এর ইতিহাসে আচরণটি বদলেছে। React 16-এর সময়ে mismatch-এ শুধু warning দিত আর নিজের মতো কিছু অংশ ঠিক করত। ফল হতো অদ্ভুত: কখনো লেখা এক জায়গায়, attribute আরেক element-এর, আর handler ভুল node-এ। এমন বাগ ধরা খুব কঠিন, কারণ পাতা দেখতে প্রায় ঠিক। React 18 থেকে নীতি কড়া হলো: লেখা না মিললে নিকটতম Suspense boundary পর্যন্ত, বা না থাকলে root পর্যন্ত, client-এ নতুন করে render. আমাদের run-এর বার্তা “this tree will be regenerated on the client” ঠিক এটিই বলে। নিরাপদ, কিন্তু খরচ: server-এর কাজ বৃথা, আর পর্দায় ঝলক।

Attribute আলাদা দলে। বাড়তি class বা data- attribute সাধারণত পাতার অর্থ বদলায় না, আর browser extension প্রায়ই এগুলোই যোগ করে। তাই React 19 শুধু জানায় “This won't be patched up” আর এগিয়ে যায়। কিন্তু href বা value-এর মতো attribute ভুল থাকলে ব্যবহারকারী ভুল জায়গায় যেতে পারেন। React ধরে নেয়, এটি আপনার বাগ, আর আপনি ঠিক করবেন।

একটি বড় শিক্ষা: “নিজে ঠিক হয়ে যায়” এমন ব্যবস্থা বাগ লুকায়। React error দেখায়, কারণ লুকানো ভুল বছরের পর বছর থেকে যায়।

মূল উপলব্ধি: Hydration একটি চুক্তি, মেরামতের যন্ত্র নয়। চুক্তি ভাঙলে framework নিরাপদ পথে পিছিয়ে যায় আর আপনাকে জানায়; আসল মেরামত কোডে।

প্র ০৩বাংলা সাইটে locale আর time zone-এর mismatch এত সহজে আসে কেন? এটি কি শুধু hydration-এর সমস্যা?

না, এটি আসলে বিতরণকৃত ব্যবস্থার একটি পুরোনো সমস্যা, hydration শুধু এটিকে চোখের সামনে আনে। একই কোড দুটি আলাদা যন্ত্রে চলছে, আর দুটির “পরিবেশ” আলাদা: ভাষা, অঞ্চল, ঘড়ি। Server সাধারণত কোনো data center-এ, অনেক সময় UTC-তে চলে, আর তার ডিফল্ট locale প্রায়ই en-US. ব্রাউজার চলে ব্যবহারকারীর ফোনে, ঢাকায়, বাংলা সেটিংসে। আমাদের run-এ দুটিই দেখা গেছে: ডিফল্ট locale-এ 1,250 বনাম ১,২৫০, আর স্পষ্ট bn-BD দেওয়ার পরেও time zone-এ তারিখ বদলে গেছে।

JavaScript-এর Intl API (ECMA-402, প্রথম সংস্করণ ২০১২) এই বিন্যাস ICU নামের একটি library আর CLDR নামের ডেটা থেকে নেয়। Node আর ব্রাউজার আলাদা ICU version নিয়ে আসতে পারে। তাই locale আর time zone দুটিই স্পষ্ট দিলেও, খুব সূক্ষ্ম ক্ষেত্রে, যেমন AM/PM-এর বাংলা রূপ বা কোনো নতুন নিয়মে, দুই দিকের লেখা আলাদা হতে পারে। আমাদের যাচাইয়ে Node 24 আর Edge একই লেখা দিয়েছে, কিন্তু এটি সব version-এর জন্য গ্যারান্টি নয়।

সমাধানের দর্শন ব্যাকএন্ডের পুরোনো নিয়মের মতো: সময় সংরক্ষণ করুন UTC-তে, দেখানোর মুহূর্তে একবার রূপান্তর করুন, আর সেই রূপান্তর এক জায়গায় করুন। Hydration-এর জন্য সবচেয়ে নিরাপদ হলো server-এ একবার লেখা বানিয়ে পাঠানো, client-এ আবার হিসাব না করা। Server component এখানে আদর্শ, কারণ সেটি ব্রাউজারে আবার চলেই না। আর যদি ব্যবহারকারীর নিজের time zone দেখাতে চান, তবে সেটি client-এর জিনিস: effect-এ বা <time> element-এ suppressHydrationWarning দিয়ে, docs-এর উদাহরণের মতো।

বাংলা অঙ্ক, তারিখ আর locale routing নিয়ে পুরো আলোচনা পর্ব ৫৯-এ, আর JavaScript-এর Intl JavaScript কোর্সে।

মূল উপলব্ধি: “এখন কয়টা বাজে” আর “কোন ভাষায়” প্রশ্নের উত্তর যন্ত্র ভেদে আলাদা। যে লেখা দুই জায়গায় তৈরি হয়, তার সব ইনপুট স্পষ্ট করে দিন।

প্র ০৪Selective hydration ক্লিককে অগ্রাধিকার দেয়। তাহলে সব কিছু ছোট ছোট Suspense-এ ভাগ করলেই কি সমস্যা শেষ?

না, আর আমাদের মাপ দেখায় কেন। React 18-এর নকশা (working group-এ ২০২১ সালের “New Suspense SSR Architecture” আলোচনায় ব্যাখ্যা করা) পুরোনো তিনটি “সব-বা-কিছুই-না” সমস্যা ভাঙতে চেয়েছিল: সব ডেটা না আসা পর্যন্ত HTML পাঠানো যেত না, সব JavaScript না আসা পর্যন্ত hydrate করা যেত না, আর সব hydrate না হওয়া পর্যন্ত কিছুই সচল হতো না। Suspense boundary প্রতিটিকে টুকরো করে দেয়।

কিন্তু টুকরো করারও সীমা আছে। এক, JavaScript নামার আগে কিছুই হয় না। আমাদের প্রথম মাপে 520 থেকে প্রায় 1080 ms পর্যন্ত ক্লিক হারিয়েছে, কারণ তখন React-ই ছিল না। Selective hydration সেখানে সাহায্য করে না। দুই, প্রতিটি boundary-র নিজস্ব খরচ: fallback, বাড়তি HTML চিহ্ন, আর আলাদা আলাদা পর্দা বদল। অনেক ছোট boundary থাকলে পাতা ঝিকমিক করে, যা পর্ব ২৩-এ দেখা “কার্ড-প্রতি Suspense”-এর মতো সমস্যা। তিন, মোট কাজ কমে না। আমাদের দ্বিতীয় পরীক্ষায় ক্লিক করলে related আগে এল, কিন্তু reviews পিছিয়ে 969 ms-এ গেল। কাজের ক্রম বদলেছে, পরিমাণ নয়।

আরেকটি সূক্ষ্ম বিষয়: replay শুধু নির্দিষ্ট ধরনের event-এ, যেমন ক্লিক; React সব event-কে একভাবে দেখে না। আর ব্যবহারকারী একটি অংশে ক্লিক করার পর যদি অন্য অংশের state-এর ওপর নির্ভর করেন, সেটি তখনো hydrate না-ও হতে পারে।

ভালো নীতি হলো boundary বসানো অর্থের ভিত্তিতে: যে অংশ আলাদাভাবে ধীর বা আলাদাভাবে গুরুত্বপূর্ণ, সেখানে। “বাজারঘর”-এ কার্টের অংশ, রিভিউ আর সম্পর্কিত পণ্য, তিনটি যুক্তিসঙ্গত ভাগ।

মূল উপলব্ধি: Selective hydration অগ্রাধিকার ঠিক করে, খরচ কমায় না। খরচ কমাতে কম JavaScript লাগে; ক্রম ঠিক করতে boundary.

প্র ০৫Islands আর resumability যদি hydration-এর খরচ এত কমায়, তাহলে সবাই React ছেড়ে চলে যাচ্ছে না কেন?

প্রযুক্তি বাছাই কখনো শুধু একটি মাপে হয় না। Islands-এর ধারণা ২০১৯–২০ সালের দিকে জনপ্রিয় হয়, Astro ২০২২ সালে 1.0-তে পৌঁছায়। Qwik-এর resumability ২০২৩ সালে 1.0 পায়। দুটিই প্রথম লোডে চমৎকার ফল দেখাতে পারে, বিশেষ করে লেখা-প্রধান পাতায়। কিন্তু একটি দল কী বাছবে, তা ঠিক করে আরও কয়েকটি জিনিস।

প্রথমত, পাতার ধরন। ব্লগ, ডকুমেন্টেশন আর মার্কেটিং পাতায় interactive অংশ কম, তাই islands স্বাভাবিক মিল। কিন্তু ড্যাশবোর্ড, চ্যাট বা জটিল কার্ট-চেকআউটে প্রায় সব অংশ interactive আর একে অন্যের state ভাগ করে। সেখানে “দ্বীপ” এত বেশি যে লাভ কমে যায়। দ্বিতীয়ত, ecosystem. React-এর সাথে বিশাল component library, টুল, শেখার উপকরণ আর নিয়োগের বাজার আছে। বাংলাদেশেও চাকরির বিজ্ঞাপনে React-এর নাম অনেক বেশি দেখা যায়, যা একটি দলের জন্য বাস্তব বিবেচনা। তৃতীয়ত, React নিজেও বদলেছে। Server component, streaming আর selective hydration মিলে islands-এর কিছু লাভ React-এর ভেতরেই এনেছে, যদিও আমাদের মাপ দেখায় সব নয়।

আবার ধারণাগুলো একে অন্যের মধ্যে ঢুকছে। Astro-র দ্বীপ হিসেবে React component চালানো যায়, অর্থাৎ বাছাই সবসময় “এটা বা ওটা” নয়। আর resumability-র মতো ধারণা ভবিষ্যতে অন্য framework-এও আসতে পারে, যদিও কেউ নিশ্চিত নয়।

কম্পিউটার বিজ্ঞানের একটি পুরোনো শিক্ষা: কাজ কোথায় হবে, server না client, compile সময় না চলার সময়, এই সিদ্ধান্তে কোনো চিরস্থায়ী উত্তর নেই। নেটওয়ার্ক, ফোনের ক্ষমতা আর ব্যবহারকারীর আচরণ বদলালে ভারসাম্যও বদলায়।

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

অনুশীলন

হিসাব, অনুমান আর নকশা, তিন রকম কাজ। আগে নিজে চেষ্টা করুন।

  1. মৃত সময়ের হিসাব: নেটওয়ার্ক 250 kB/s, RTT 100 ms. Paint-এর আগে ৩টি RTT আর 10 kB HTML. তারপর ১টি RTT আর 150 kB JavaScript (transfer) নামানো। Decoded JS 500 kB, চালানোর খরচ 0.8 ms/kB. ৮০০টি element, প্রতিটিতে 0.1 ms. Paint, interactive আর মৃত সময় কত? মৃত সময় 1000 ms-এর নিচে আনতে decoded JS কতটা কমাতে হবে?

    Paint: 3 × 100 + 10 ÷ 250 × 1000 = 300 + 40 = 340 ms. JS প্রস্তুত: 340 + 100 + 150 ÷ 250 × 1000 = 340 + 100 + 600 = 1040 ms. চালানো 500 × 0.8 = 400 ms, মেলানো 800 × 0.1 = 80 ms. Interactive = 1040 + 400 + 80 = 1520 ms. মৃত সময় 1520 − 340 = 1180 ms. 1000-এর নিচে আনতে 180 ms-এর বেশি কমাতে হবে। শুধু চালানোর অংশ থেকে: 180 ÷ 0.8 = 225 kB, মানে decoded JS 275 kB-এর নিচে। তবে JS কমালে transfer-ও কমে, তাই বাস্তবে এর চেয়ে কম কাটলেই চলবে।

  2. আউটপুট অনুমান: একটি client component-এ const [n, setN] = useState(0); useEffect(() => { setN(5); }, []); console.log('render', n); আর return <p>{n}</p>;. Next.js production build-এ পাতাটি static. Server-এর HTML-এ <p>-এর ভেতরে কী থাকবে? ব্রাউজারের console-এ কী কী লাইন আসবে? কোনো hydration error হবে কি?

    HTML-এ <p>0</p>, কারণ server-এ effect চলে না। ব্রাউজারের console-এ render 0 (hydration-এর render), তারপর effect চলে setN(5), তাই render 5. Production-এ StrictMode-এর দ্বিগুণ render নেই। কোনো error নেই, কারণ hydration-এর প্রথম render server-এর মতোই 0 দিয়েছে। Build-এর সময় server-এর console-এ render 0 একবার এসেছিল, কিন্তু সেটি ব্রাউজারে দেখা যায় না।

  3. নিজে চালিয়ে দেখুন: একটি নতুন Next.js প্রজেক্টে এই পর্বের Clock component বানান। next dev-এ পাতা খুলে console-এর diff পড়ুন। তারপর next build চালিয়ে route তালিকায় পাতার চিহ্ন দেখুন, আর next start-এ একই পাতা খুলে error-এর রূপ লিখুন। দুই বার্তার পার্থক্য কী, আর কেন?

    Dev-এ আসে লম্বা “Hydration failed because the server rendered text didn't match the client” বার্তা, কারণের তালিকা আর +/- diff. Route তালিকায় পাতাটি সম্ভবত ○ (Static), কারণ এতে dynamic কিছু নেই; ফলে HTML-এর সময় build-এর মুহূর্তের। Production-এ আসে “Minified React error #418” আর একটি link, কারণ production React-এ বার্তার লেখা বাদ দিয়ে আকার ছোট রাখা হয়। পার্থক্যের সংখ্যা production-এ অনেক বড়, build আর পাতা খোলার মাঝের পুরো সময়। Version-ভেদে বার্তার শব্দ সামান্য বদলাতে পারে।

  4. নকশা সমালোচনা: একটি দল “বাজারঘর”-এর হোম পাতার শুরুতে 'use client' লিখেছে, পুরো পাতা একটি <Suspense>-এ মোড়া, কার্টের সংখ্যা দেখাতে render-এ সরাসরি localStorage.getItem('cart') পড়ে, আর hydration error এলে body-তে suppressHydrationWarning দিয়েছে। কী কী সমস্যা? আপনি কীভাবে সাজাবেন?

    সমস্যা: পুরো পাতা client component, তাই সব কোড ব্রাউজারে যায় আর সব hydrate হয়। একটিমাত্র Suspense মানে selective hydration-এর কোনো ভাগ নেই। Render-এ localStorage server-এ নেই, তাই error বা typeof window-এর শাখা লাগবে, যা mismatch আনে। Body-র suppressHydrationWarning এক স্তরেই কাজ করে, ভেতরের অমিল লুকায় না, আর আসল কারণ ঠিক করে না। সাজানো: পাতা server component; কার্টের ব্যাজ ছোট client component, যা useSyncExternalStore দিয়ে store পড়ে আর server snapshot-এ ০ বা “—” দেয়; লগইন করা ব্যবহারকারীর কার্ট server থেকে আনা যায়। ধীর অংশে (সুপারিশ, রিভিউ) আলাদা Suspense. Body-র attribute warning শুধু extension-এর কারণে হলে বোঝা যায়, কিন্তু সেটিও বুঝে-শুনে দিন।

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

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

ক্রমSSR করা একটি React পাতা খোলার ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Server component tree চালিয়ে HTML বানায়
  2. ব্রাউজার HTML আঁকে, বোতাম দেখা যায়
  3. JavaScript নামে আর চলতে শুরু করে
  4. hydrateRoot ডাকা হয়, root-এ listener বসে
  5. React DOM-এর সাথে tree মিলিয়ে handler জোড়ে
  6. প্রথম ক্লিকে handler চলে

একটি Next.js client component render-এ {(1250).toLocaleString()} লেখে। Server Node-এ ডিফল্ট locale en-US, ব্যবহারকারীর ব্রাউজারে bn-BD. Hydration-এ কী ঘটবে?

  • কিছুই না, দুই দিকে 1,250
  • লেখা মেলে না, hydration error, অংশটি client-এ নতুন করে render
  • React নীরবে লেখা বদলে দেয়, কোনো বার্তা নেই
  • next build ব্যর্থ হবে

<div suppressHydrationWarning><p>{Date.now()}</p></div> লিখলে hydration-এ কী হওয়ার কথা?

  • ভেতরের সব element-এর warning বন্ধ হবে
  • React <p>-এর লেখা ঠিক করে দেবে
  • Compile error হবে
  • Warning তবু আসবে, কারণ অমিলটি ভেতরের <p>-এ

একটি পাতায় দুটি Suspense boundary, A আগে আর B পরে, দুটির HTML পর্দায় আছে কিন্তু কোনোটি hydrate হয়নি। React-এর JavaScript চলছে, আর ব্যবহারকারী B-র বোতামে ক্লিক করলেন। React 18 বা পরের version-এ সাধারণত কী হয়?

  • B আগে hydrate হয়, তারপর জমা ক্লিকের handler চলে
  • ক্লিক হারিয়ে যায়, ব্যবহারকারীকে আবার চাপতে হয়
  • A শেষ না হওয়া পর্যন্ত B অপেক্ষা করে, ক্লিক পরে চলে
  • ব্রাউজার পাতা reload করে

Next.js App Router-এ dynamic(() => import('./Map'), { ssr: false }) সম্পর্কে কোনটি ঠিক?

  • Server component-এ লিখলেই সবচেয়ে ভালো কাজ করে
  • Map-এর লেখা HTML-এ থাকে, শুধু hydration বাদ যায়
  • Map server-এ render হয় না; HTML-এ loading থাকে, পরে ব্রাউজারে আসে
  • পুরো পাতা CSR হয়ে যায়

ধরুন একটি পাতার paint 400 ms-এ। তারপর JavaScript প্রস্তুত 1100 ms-এ, চালাতে 300 ms আর hydrate করতে 100 ms লাগে। মৃত সময় (paint থেকে সচল পর্যন্ত) কত?

  • 700 ms
  • 1100 ms
  • 1500 ms
  • 400 ms

React-এর useSyncExternalStore-এর তৃতীয় argument getServerSnapshot কখন চলে?

  • প্রতিটি client render-এ
  • শুধু store বদলালে
  • Server-এ HTML বানানোর সময় আর client-এ hydration-এর সময়
  • Component unmount হওয়ার সময়

শব্দকোষ

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

মনে রাখুন

  1. Hydration = জোড়া: Server-এর HTML-এর সাথে client-এর প্রথম render মিলিয়ে handler বসানো। প্রথম render হুবহু server-এর মতো হতে হবে।
  2. মৃত সময় আসল: আমাদের এক run-এ paint 520 ms, প্রথম সফল ক্লিক 1418 ms. JS আসার আগে ক্লিক হারায়, hydration চলাকালীন লাইনে দাঁড়ায়।
  3. Mismatch-এর উৎস: সময়, random, typeof window, locale ও time zone, ভুল nesting, extension. Dev-এর diff-এ + client, - server.
  4. সমাধানের ক্রম: একই জিনিস render → effect বা useSyncExternalStore → এক element-এ suppressHydrationWarning → শেষে ssr: false.
  5. খরচ কমান মেপে: Suspense ক্রম ঠিক করে, server component কোড সরায়, islands আর resumability অন্য পথ। একটি লেখা বাজেট রাখুন।

পরের পর্বে: Next.js-এর backend দিক: route handler দিয়ে API বানানো, আর Next.js 16-এ proxy.ts দিয়ে প্রতিটি অনুরোধের আগে কাজ চালানো।

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

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