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

Virtual DOM আর reconciliation — diff, Fiber আর key-এর আসল কাজ

Virtual DOM and Reconciliation — Diffing, Fiber and What Keys Really Do

Taught in Bangla, this lesson explains what the virtual DOM is and is not, how two assumptions turn tree diffing from O(n^3) into O(n), why state lives at a position and type, counting list diffs to see what keys really do, Fiber splitting work into pieces, priorities and concurrent features, batching and why a re-render need not change the DOM, separating render and commit in the Profiler, and the no-virtual-DOM approach of Solid and Svelte.

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

  • Plain JavaScript-এ একটি ছোট el() আর diff() বানিয়ে reconciliation বোঝা
  • State কখন থাকে, কখন হারায়, আর key দিয়ে তা নিয়ন্ত্রণ করা
  • Fiber, render, commit, priority আর batching-এর আসল আচরণ
  • MutationObserver আর Profiler দিয়ে render আর আসল DOM বদল আলাদা করে মাপা
  • Virtual DOM-এর সাথে Solid আর Svelte-এর পথের তুলনা

১ · Virtual DOM আসলে কী, আর কী নয়

মডিউল ২-এর শুরু থেকে আপনি একটি কথা বারবার দেখেছেন, বিশেষ করে পর্ব ১০-এ: state বদলালে React component ফাংশন আবার চালায়, আর স্ক্রিন ঠিক হয়ে যায়। এই পর্বে সেই “ঠিক হয়ে যায়” অংশটি খুলে দেখব। প্রশ্ন একটাই: ফাংশন তো নতুন JSX ফেরত দিল, তারপর React কীভাবে বুঝল আসল DOM-এ ঠিক কোন জায়গাটি বদলাতে হবে?

“বাজারঘর”-এর গুদামের কথা ভাবুন। তাকে হাজার পণ্য সাজানো। তাক গোছানো ভারী কাজ, তাই ম্যানেজার প্রতিদিন সব নামিয়ে আবার সাজান না। তিনি খাতায় লেখেন, আজ তাকে কী কী থাকা উচিত। তারপর গতকালের খাতার সাথে মিলিয়ে দেখেন। শুধু যে দুটি লাইন আলাদা, কর্মীকে সেই দুটি জিনিস বদলাতে বলেন।

খাতা হলো virtual DOM, তাক হলো আসল DOM, আর দুই খাতা মেলানোর কাজের নাম reconciliation.

বাস্তবে “virtual DOM” কোনো আলাদা জিনিস বা ব্রাউজারের ফিচার নয়। JSX compile হয়ে একটি ফাংশন-ডাকে পরিণত হয়, আর সেই ডাকের ফল একটি React element, মানে সাধারণ object:

// JSX
<h3 className="name">মসুর ডাল</h3>

// React এর থেকে যে object পায় (সংক্ষেপে)
{ type: 'h3', key: null, props: { className: 'name', children: 'মসুর ডাল' } }

এই object বানানো খুব সস্তা। এতে কোনো layout নেই, style নেই, পর্দায় কিছু আঁকা নেই। React-এর পুরোনো ডকুমেন্টেশনের ভাষায় virtual DOM একটি “programming concept”: UI-এর একটি আদর্শ রূপ মেমরিতে রাখা, আর আসল DOM-এর সাথে তাকে মিলিয়ে নেওয়া।

সবচেয়ে প্রচলিত ভুল ধারণা: “Virtual DOM আসল DOM-এর চেয়ে দ্রুত।” কথাটি অর্থহীন। Virtual DOM শেষ পর্যন্ত আসল DOM-কেই বদলায়। হাতে একটি লেখা বদলাতে লাগে একটি DOM কাজ। React-এ ওই একই বদলে লাগে সেই একটি DOM কাজ, তার সাথে ফাংশন চালানো আর তুলনা করার খরচ। তাই সঠিকভাবে লেখা হাতের কোডের চেয়ে React দ্রুত হতে পারে না।

তাহলে লাভ কোথায়? লাভটি গতিতে নয়, declarative কোড লেখার সুযোগে। আপনি বলেন “স্ক্রিন কেমন হবে”, আর একটি যুক্তিসংগত খরচে React বাকিটা সামলায়। React না থাকলে দুটি পথ থাকত: প্রতিবার পুরো innerHTML বদলানো (ধীর, input-এর focus হারায়), অথবা হাতে প্রতিটি জায়গা মেলানো (ভুলের ঝুঁকি)। Virtual DOM এই দুইয়ের মাঝের পথ।

React ২০১৩ সালে উন্মুক্ত হয়, তখন “পুরো UI আবার render করুন” ধারণাটি অনেকের কাছে অদ্ভুত লেগেছিল। Virtual DOM সেটিকে বাস্তবসম্মত করে। পরে ২০১৮ সালে Svelte-এর স্রষ্টা Rich Harris “Virtual DOM is pure overhead” লেখায় যুক্তি দেন যে virtual DOM “সাধারণত যথেষ্ট দ্রুত”, কিন্তু এটি কোনো ফিচার নয়, একটি উপায় মাত্র।

২ · Diff-এর অঙ্ক: O(n³) থেকে O(n)

দুটি tree-র মধ্যে সবচেয়ে কম পরিবর্তনের তালিকা বের করা একটি পরিচিত গণিতের সমস্যা, নাম tree edit distance. React-এর ডকুমেন্টেশন অনুযায়ী এর সেরা সাধারণ algorithm-এর জটিলতা প্রায় O(n³). Big-O নিয়ে বিস্তারিত আছে DSA কোর্সে। এখানে শুধু হিসাবটি দেখি।

node (n)O(n³) তুলনাধরে নেওয়া সময়O(n) তুলনাধরে নেওয়া সময়
১০০১০ লাখ10 ms১০০1 µs
১,০০০১০০ কোটি10 s১,০০০10 µs
১০,০০০১০ লাখ কোটিপ্রায় 2.8 ঘণ্টা১০,০০০100 µs

এখানে ধরে নিয়েছি, সেকেন্ডে ১০ কোটি (10⁸) তুলনা হয়; এটি শুধু হিসাবের সুবিধার জন্য, মাপা সংখ্যা নয়। ১,০০০ node-এর জন্য 10⁹ ÷ 10⁸ = 10 s. অথচ ৬০ Hz পর্দায় একটি ফ্রেমের বাজেট মাত্র প্রায় 16.7 ms. তাই নিখুঁত উত্তর খোঁজা এখানে চলবে না। React তাই নিখুঁত হওয়ার দাবি ছেড়ে দুটি heuristic বা বাস্তবসম্মত অনুমান নেয়:

অনুমান ১: ধরন আলাদা, tree আলাদা

একই জায়গায় button-এর বদলে b এলে React ধরে নেয়, ভেতরের সবকিছুই আলাদা। পুরোনো subtree ফেলে নতুনটি বানায়। নিচে নেমে তুলনা করে না।

Type comparison

অনুমান ২: key পরিচয় দেয়

List-এ key বলে দেয়, কোন উপাদান আগের কোনটি। ক্রম বদলালেও React চিনে নেয়।

Developer hint

ধরন এক হলে কী হয়? DOM element হলে React একই node রাখে, শুধু বদলানো attribute পাল্টায়। Component হলে একই instance থাকে, তাই state-ও থাকে, আর তার ফাংশন নতুন props নিয়ে চলে। তারপর React children-এ নামে। প্রতিটি node মাত্র একবার দেখা হয়, তাই কাজটি O(n).

এবার নিজে বানিয়ে দেখি। নিচের el() একটি ছোট element বানায়, আর diff() দুটি tree মিলিয়ে একটি patch তালিকা দেয়। “বাজারঘর”-এর একটি পণ্য কার্ডে ছাড় পড়েছে, দাম কমেছে, আর স্টক শেষ। Run চাপুন।

// el(): React element-এর ছোট সংস্করণ, শুধুই একটি object
const el = (type, props, ...kids) => ({ type, props: props || {}, kids });
let visited = 0;

function diff(a, b, path, out) {
  visited++;
  if (a === undefined) { out.push(`INSERT ${path}`); return out; }
  if (b === undefined) { out.push(`REMOVE ${path}`); return out; }
  if (typeof a === 'string' && typeof b === 'string') {
    if (a !== b) out.push(`TEXT ${path}: ${a} -> ${b}`);
    return out;
  }
  if (a.type !== b.type) { // ধরন আলাদা: নিচে আর নামবে না
    out.push(`REPLACE ${path}: ${a.type} -> ${b.type}`);
    return out;
  }
  const names = new Set([...Object.keys(a.props), ...Object.keys(b.props)]);
  for (const k of names) {
    if (a.props[k] !== b.props[k]) out.push(`PROP ${path} ${k}: ${a.props[k]} -> ${b.props[k]}`);
  }
  const n = Math.max(a.kids.length, b.kids.length);
  for (let i = 0; i < n; i++) diff(a.kids[i], b.kids[i], `${path}/${i}`, out);
  return out;
}

const before = el('div', { class: 'card' },
  el('h3', null, 'মসুর ডাল'), el('p', null, '৳120'), el('button', null, 'যোগ করুন'));
const after = el('div', { class: 'card sale' },
  el('h3', null, 'মসুর ডাল'), el('p', null, '৳110'), el('b', null, 'স্টক শেষ'));

const patches = diff(before, after, 'root', []);
patches.forEach(p => console.log(p));
console.log('দেখা node:', visited, '| patch:', patches.length);

আউটপুট:

PROP root class: card -> card sale
TEXT root/1/0: ৳120 -> ৳110
REPLACE root/2: button -> b
দেখা node: 6 | patch: 3

root/1/0 মানে root-এর ২য় সন্তানের (index 1) প্রথম সন্তান, অর্থাৎ দামের লেখা। ৬টি node দেখে ৩টি patch পাওয়া গেল। h3-এর জন্য কোনো patch নেই, কারণ কিছু বদলায়নি। button আর b-এর ধরন আলাদা, তাই ভেতরের লেখা “স্টক শেষ” তুলনাই হয়নি। React-এর আসল কোড অনেক বড়, তবে মূল যুক্তি এটিই।

একই diff ধাপে ধাপে দেখুন। প্রতিটি ধাপে পরে চেপে patch তালিকা কীভাবে বাড়ে, খেয়াল করুন।

1// আগের tree2div.card3  h3: মসুর ডাল4  p: ৳1205  button: যোগ করুন6// নতুন tree7div.card.sale8  h3: মসুর ডাল9  p: ৳11010  b: স্টক শেষ
diff-এর অবস্থা
দেখা node
তুলনা
patch সংখ্যা
  1. Root তুলনা। দুই দিকেই div, তাই DOM node থাকবে। কিন্তু class আলাদা, তাই প্রথম patch: PROP class.
  2. প্রথম সন্তান। দুই দিকেই h3. Attribute একই, কোনো patch নেই।
  3. h3-এর লেখা। দুই দিকেই “মসুর ডাল”, তাই এখানেও patch নেই। দেখা node এখন ৩টি।
  4. দ্বিতীয় সন্তান। দুই দিকেই p. Node থাকবে, এবার ভেতরে নামতে হবে।
  5. দামের লেখা। ৳120 আর ৳110 আলাদা। দ্বিতীয় patch: TEXT.
  6. তৃতীয় সন্তান। button আর b, ধরন আলাদা। তৃতীয় patch: REPLACE. ভেতরের লেখায় আর নামা হলো না।
  7. শেষ। ৬টি node দেখে ৩টি patch. প্রতিটি node একবারই দেখা হয়েছে, তাই কাজ node-সংখ্যার সমানুপাতিক।

৩ · State থাকে জায়গায়: position, ধরন আর subtree ধ্বংস

Reconciliation-এর একটি ফল প্রথমে অবাক করে। useState-এর মান component ফাংশনের ভেতরে থাকে না। React সেটি রাখে tree-র একটি নির্দিষ্ট জায়গায়। React দেখে, এই জায়গায় আগের বার কী ছিল, এবার কী এল।

পরিস্থিতিState-এর কী হয়
একই জায়গায় একই ধরন, props আলাদাথাকে
একই জায়গায় ভিন্ন ধরন (A থেকে B)হারায়, পুরো subtree ধ্বংস
বাইরের wrapper বদলানো (div থেকে section)ভেতরের সবার হারায়
Component সরে গিয়ে পরে ফেরাহারায়, নতুন করে শুরু
একই জায়গায় key বদলানোহারায়, নতুন instance
List-এ জায়গা বদল, key একইথাকে, উপাদানের সাথে সরে যায়
Component-এর ভেতরে আরেকটি component সংজ্ঞায়িতপ্রতি render-এ হারায়

শেষ সারিটি একটি নীরব বাগ। ভেতরে লেখা ফাংশন প্রতি render-এ নতুন ফাংশন হয়, তাই React-এর চোখে “ধরন” বদলায়, যদিও নাম একই। ফলে input-এ প্রতিটি অক্ষর লেখার পর focus চলে যায়। Component সবসময় বাইরে, ফাইলের শীর্ষ স্তরে লিখুন।

নিচের live demo-তে দুটি list আছে। একটিতে key হলো পণ্যের নাম, আরেকটিতে index. Run চাপুন, প্রতিটি list-এ “ডাল +১” দুবার চাপুন, তারপর “উল্টান” চাপুন। শেষে “Wrapper বদলান” চাপুন।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
function Qty({ name, tag }) {
  const [n, setN] = useState(0);
  useEffect(() => {
    console.log('mount', tag, name);
    return () => console.log('unmount', tag, name);
  }, []);
  return <p><button onClick={() => setN(n + 1)}>{name} +১</button> সংখ্যা: {n}</p>;
}
function App() {
  const [names, setNames] = useState(['ডাল', 'চাল']);
  const [boxed, setBoxed] = useState(false);
  const Wrap = boxed ? 'section' : 'div';
  return (
    <Wrap>
      <h4>key = নাম</h4>
      {names.map(x => <Qty key={x} name={x} tag="নাম" />)}
      <h4>key = index</h4>
      {names.map((x, i) => <Qty key={i} name={x} tag="index" />)}
      <button onClick={() => setNames([...names].reverse())}>উল্টান</button>
      <button onClick={() => setBoxed(!boxed)}>Wrapper বদলান</button>
    </Wrap>
  );
}
createRoot(document.getElementById('root')).render(<App />);
</script>

Run করলে আসে mount নাম ডাল, mount নাম চাল, mount index ডাল, mount index চাল. আমরা পাতাটি Edge-এ চালিয়ে দুই list-এ “ডাল +১” দুবার করে চেপে “উল্টান” চেপেছি। পর্দায় এই ফল এসেছে:

key = নাম:    চাল সংখ্যা: 0 · ডাল সংখ্যা: 2
key = index:  চাল সংখ্যা: 2 · ডাল সংখ্যা: 0

“উল্টান”-এ কোনো নতুন mount বা unmount লাইন আসেনি। নাম-key-তে ডালের 2 ডালের সাথেই নিচে নেমেছে। Index-key-তে নাম বদলেছে, কিন্তু 2 প্রথম জায়গাতেই রয়ে গেছে, তাই এখন চালের পাশে 2 দেখাচ্ছে। এই demo-তে ক্ষতি নেই, কিন্তু আসল কার্টে এটি ভুল অর্ডার।

এরপর “Wrapper বদলান” চাপলে div থেকে section হয়, আর লগে আসে:

unmount নাম চাল
unmount নাম ডাল
unmount index ডাল
unmount index চাল
mount নাম চাল
mount নাম ডাল
mount index চাল
mount index ডাল

Key একই থাকলেও চারটি Qty-ই ধ্বংস হয়েছে আর সব সংখ্যা 0 হয়েছে, কারণ ওপরের ধরন বদলানোয় পুরো subtree গেছে। তৃতীয় লাইনে “ডাল” এসেছে, কারণ cleanup ফাংশন mount-এর সময়ের নাম মনে রেখেছে; effect-এর খালি dependency-র ফল। Component সরে যাওয়াকে বলে unmount.

এই demo একটি কাজের কৌশলও শেখায়। Chat বা প্রোফাইল ফর্মে অন্য ব্যবহারকারী বেছে নিলে পুরোনো draft থেকে যাওয়া একটি পরিচিত বাগ। সমাধান একটি শব্দ: <ProfileForm key={user.id} />. Key বদলালে React পুরো form নতুন করে বানায়, আলাদা effect দিয়ে state মুছতে হয় না।

JSX-এর শর্তে {show && <Banner />} লিখলে show মিথ্যা হলেও ওই জায়গাটি খালি ঘর হিসেবে থাকে। তাই তার পরের সহোদর একই জায়গায় থাকে, state হারায় না। জায়গার হিসাব হয় render-এর ফলের tree দিয়ে, আপনার কোডের লাইন দিয়ে নয়।

৪ · key-এর আসল কাজ: list-এর diff গুনে দেখা

পর্ব ১১-এ আপনি জেনেছেন list-এ key দিতে হয়। এবার দেখি কেন। স্কুলের ক্লাসের কথা ভাবুন। স্যার যদি “প্রথম বেঞ্চের ছাত্র” বলে ডাকেন, ছাত্ররা জায়গা বদলালে ভুল মানুষকে ডাকা হবে। রোল নম্বর ধরে ডাকলে এই সমস্যা নেই। Index হলো বেঞ্চের ক্রম, স্থায়ী key হলো রোল নম্বর।

নিচের কোড দুইভাবে list মেলায় আর কাজ গোনে। Key-সহ অংশটি React-এর আসল নিয়মের সরল রূপ।

// key ছাড়া: একই index-এর জিনিস তুলনা
function unkeyed(a, b) {
  let text = 0;
  for (let i = 0; i < Math.min(a.length, b.length); i++) if (a[i] !== b[i]) text++;
  return { text, insert: Math.max(b.length - a.length, 0), remove: Math.max(a.length - b.length, 0) };
}

// key সহ: React-এর lastPlacedIndex নিয়মের সরল রূপ
function keyed(a, b) {
  const old = new Map(a.map((k, i) => [k, i]));
  let last = 0, move = 0, insert = 0;
  for (const k of b) {
    if (!old.has(k)) { insert++; continue; }
    const i = old.get(k);
    old.delete(k);
    if (i < last) move++; else last = i;
  }
  return { move, insert, remove: old.size };
}

const show = (name, a, b) => {
  console.log(name);
  console.log('  key নেই:', JSON.stringify(unkeyed(a, b)));
  console.log('  key আছে:', JSON.stringify(keyed(a, b)));
};

const abcd = ['A', 'B', 'C', 'D'];
const list = Array.from({ length: 1000 }, (_, i) => 'p' + i);
show('১০০০টি, শুরুতে ১টি নতুন', list, ['new', ...list]);
show('১০০০টি, শেষে ১টি নতুন', list, [...list, 'new']);
show('D A B C', abcd, ['D', 'A', 'B', 'C']);
show('B C D A', abcd, ['B', 'C', 'D', 'A']);
show('D C B A', abcd, ['D', 'C', 'B', 'A']);

আউটপুট:

১০০০টি, শুরুতে ১টি নতুন
  key নেই: {"text":1000,"insert":1,"remove":0}
  key আছে: {"move":0,"insert":1,"remove":0}
১০০০টি, শেষে ১টি নতুন
  key নেই: {"text":0,"insert":1,"remove":0}
  key আছে: {"move":0,"insert":1,"remove":0}
D A B C
  key নেই: {"text":4,"insert":0,"remove":0}
  key আছে: {"move":3,"insert":0,"remove":0}
B C D A
  key নেই: {"text":4,"insert":0,"remove":0}
  key আছে: {"move":1,"insert":0,"remove":0}
D C B A
  key নেই: {"text":4,"insert":0,"remove":0}
  key আছে: {"move":3,"insert":0,"remove":0}

শুরুতে একটি পণ্য যোগ করলে key ছাড়া ১,০০০টি লেখা বদলাতে হয়, key-সহ মাত্র ১টি insert. শেষে যোগ করলে দুটিই সমান, তাই index key অনেক সময় “কাজ করছে” বলে মনে হয়।

সবচেয়ে মজার তুলনা D A B C আর B C D A. দুটিই একটি উপাদানের জায়গা বদল, অথচ প্রথমটিতে ৩টি move, দ্বিতীয়টিতে ১টি। আমরা আসল React 19.3.1-এ insertBefore গুনেও একই ৩ আর ১ পেয়েছি (একটি স্থানীয় run).

৩টি move কেন লাগল? React সামনের দিক থেকে হাঁটে আর lastPlacedIndex নামের একটি সংখ্যা মনে রাখে। অর্থ: এখন পর্যন্ত যাদের জায়গায় রাখা হয়েছে, তাদের সবচেয়ে বড় পুরোনো index. কোনো উপাদানের পুরোনো index এর চেয়ে ছোট হলে সেটি সরাতে হবে।

1// পুরোনো: A=0 B=1 C=2 D=32// নতুন ক্রমে হাঁটা:3D4A5B6C7commit: DOM-এ বসানো
Reconciler-এর হিসাব
পুরোনো index
lastPlacedIndex
সিদ্ধান্ত
move
  1. শুরু। lastPlacedIndex = 0. পুরোনো উপাদানগুলো key ধরে খুঁজে পাওয়ার ব্যবস্থা হয়েছে।
  2. D. পুরোনো index 3, আর 3 ≥ 0. D জায়গায় থাকবে। lastPlacedIndex হলো 3. এই এক ধাপই পরের সবাইকে “পেছনে” ফেলে দিল।
  3. A. পুরোনো index 0, আর 0 < 3. নতুন ক্রমে A আছে D-এর পরে, কিন্তু পুরোনো DOM-এ আগে। তাই A সরবে। move = 1.
  4. B. 1 < 3, সরবে। move = 2.
  5. C. 2 < 3, সরবে। move = 3. এখনো DOM বদলায়নি, শুধু চিহ্ন দেওয়া হয়েছে।
  6. Commit. চিহ্ন দেওয়া ৩টি node DOM-এ সরানো হলো। ক্রম হলো D A B C. মানুষ হলে শুধু D-কে সরাত, কিন্তু React-এর সামনে-থেকে-হাঁটা নিয়ম সেটি খোঁজে না।

Key বাছাইয়ের নিয়ম। Key হবে ডেটার স্থায়ী id, যেমন পণ্যের id. শুধু ভাইবোনদের মধ্যে আলাদা হলেই চলবে, পুরো অ্যাপে নয়। Math.random() বা প্রতি render-এ নতুন crypto.randomUUID() কখনো নয়: প্রতিবার key বদলালে সব উপাদান ধ্বংস হয়ে নতুন করে তৈরি হয়। Index শুধু তখন গ্রহণযোগ্য, যখন list একদম স্থির, ক্রম বদলায় না, আর উপাদানে নিজস্ব state নেই।

৫ · Fiber: কাজকে ছোট ছোট টুকরো করা

React 16-এর (২০১৭) আগে reconciliation চলত সাধারণ recursive ফাংশনে। Recursion JavaScript-এর call stack ব্যবহার করে, আর stack মাঝপথে থামিয়ে পরে ফিরে আসা যায় না। ৫০ ms-এর কাজ শুরু হলে ৫০ ms পরই ব্রাউজার ছাড়া পেত, ততক্ষণ ক্লিক আর টাইপ অপেক্ষায় থাকত।

রেস্তোরাঁর বাবুর্চি মাথায় পুরো অর্ডার রেখে এক টানে রান্না করলে মাঝখানে কারো কথা শুনতে পারবেন না। কিন্তু প্রতিটি পদ যদি আলাদা কাগজে লেখা থাকে, একটি পদ শেষ করে তিনি দেখতে পারেন জরুরি কিছু এসেছে কি না। পরে কাগজ দেখে ঠিক সেখান থেকে শুরু করবেন। React-এর ভাষায় এই কাগজের নাম Fiber.

একটি fiber হলো একটি সাধারণ JavaScript object, যা একটি component বা element-এর এক টুকরো কাজ বা unit of work বোঝায়। এতে থাকে type, key, props, state, আর সম্পর্কের pointer: child, sibling, return. কাজের হিসাব stack-এ নয়, heap-এর এই object-গুলোয় থাকে, তাই React যেকোনো দুটি fiber-এর মাঝে থামতে পারে।

React একসাথে দুটি fiber tree রাখে। current tree মানে পর্দায় যা আছে। আর work-in-progress tree মানে যা বানানো হচ্ছে। কাজ শেষ হলে React root-এর pointer বদলে নতুন tree-কে current করে দেয়। তাই ব্যবহারকারী কখনো আধা-বানানো UI দেখেন না।

একটি update-এর জীবন: render আর commit

Trigger, an interruptible render phase, an uninterrupted commit phase, then paint and effects

Update-এর পথ: trigger, render, commit, paint, effect Trigger setState বা প্রথম render Render ধাপ component ফাংশন চলে fiber tree তৈরি, diff, চিহ্ন দেওয়া থামানো ও ফেলে দেওয়া যায় DOM-এ হাত পড়ে না Commit ধাপ DOM বদল, ref বসানো useLayoutEffect চলে একটানা চলে, থামে না ব্রাউজারের paint useEffect (সাধারণত paint-এর পরে)
ছবি ১৫.১ — Render ধাপ হিসাবের খাতা, commit ধাপ তাকে আসল বদল।

এই ছবির দুটি ধাপ অনেকেই গুলিয়ে ফেলেন, কারণ দৈনন্দিন ভাষায় “render” মানে পর্দায় আঁকা। React-এর ভাষায় render মানে শুধু আপনার component ফাংশন চালানো। Render ধাপ আর commit ধাপ আলাদা, আর পর্দায় আঁকার কাজকে বলে paint. একটি ক্লিক ধরে তিনটিকে দেখি। Cart-এ সংখ্যা 1 থেকে 2 হচ্ছে।

1click: setQty(2)2Cart3  div4    h3: মসুর ডাল5    b: {qty}6commit: b.textContent7browser paint8useEffect
কোথায় কী আছে
ধাপ
state qty
DOM-এ b
পর্দায়
  1. Trigger. ক্লিকে setQty(2) চলল। ফাংশন সঙ্গে সঙ্গে আবার চলে না। React update-টি সারিতে রাখে আর একটি priority দেয়।
  2. Cart fiber. React Cart() চালায়, নতুন JSX পায়। এখন থেকে নতুন হিসাবে qty 2. DOM আর পর্দা এখনো 1.
  3. div আর h3. ধরন একই, props-এ লেখা একই। কিছু করার নেই। এটিও একটি একটি করে fiber-এর কাজ।
  4. b fiber. ভেতরের লেখা 1 থেকে 2 হয়েছে। React এই fiber-এ Update চিহ্ন দেয়। DOM-এ এখনো হাত পড়েনি।
  5. Render শেষ। Work-in-progress tree প্রস্তুত, DOM এখনো 1. এতক্ষণের যেকোনো সময় কাজ বাদ দেওয়া যেত, ব্যবহারকারী কিছুই টের পেতেন না। (ক্লিকের কাজ সাধারণত থামে না; থামে transition-এর মতো অ-জরুরি কাজ।)
  6. Commit. চিহ্ন দেওয়া জায়গায় DOM বদলাল, b এখন 2, root-এর current নতুন tree-কে দেখায়। এই ধাপ মাঝপথে থামে না। পর্দা কিন্তু এখনো 1.
  7. Paint. JavaScript শেষ হলে ব্রাউজার আঁকে। এবার পর্দায় 2.
  8. Effect. এবার useEffect চলে। ক্লিকের মতো ঘটনায় এটি paint-এর আগেও চলতে পারে, তবে সাধারণ নিয়ম হলো পরে।

Render ধাপ একাধিকবার চলতে বা ফেলে দেওয়া হতে পারে, তাই component ফাংশনকে pure রাখুন। ফাংশনের ভেতরে সরাসরি fetch, DOM বদল বা global সংখ্যা বাড়ানো নয়। এজন্যই side effect যায় event handler বা useEffect-এ, যা পর্ব ১৩-এ দেখেছেন। Development-এ StrictMode ইচ্ছা করে ফাংশন দুবার চালায়, যাতে এই ভুল ধরা পড়ে।

৬ · Priority আর concurrent feature: কোন কাজ আগে

অনুসন্ধান বাক্সে টাইপ করলে দুটি কাজ হয়: বাক্সে অক্ষর দেখানো, আর ফলের বড় তালিকা বানানো। প্রথমটি দেরি হলে ব্যবহারকারী বিরক্ত হন, দ্বিতীয়টি একটু দেরি হলে কেউ টেরও পান না। React প্রতিটি update-এ একটি অগ্রাধিকার দেয়, ভেতরের নাম lane. আর scheduler ঠিক করে, কোন কাজ কখন চলবে।

Lane (সরল করে)কখনআচরণ
SyncLaneক্লিক, কী চাপাসবার আগে, একটানা
InputContinuousLanedrag, hover-এর মতো ক্রমাগত ইনপুটউঁচু অগ্রাধিকার
DefaultLanetimer, fetch-এর পরের updateসাধারণ
TransitionLanesstartTransition-এর ভেতরেটুকরো করে চলে, থামতে পারে
IdleLaneযখন আর কিছু নেইসবার শেষে

এই থামতে পারা ক্ষমতার নাম concurrent rendering, যা React 18 (২০২২) থেকে ব্যবহারে এসেছে। নামে “concurrent” হলেও JavaScript একটি thread-এই চলে। দুটি কাজ একসাথে হয় না; একটি থামিয়ে আরেকটি চলে। এই সুবিধা পেতে একটি concurrent feature ব্যবহার করতে হয়:

function Search() {
  const [text, setText] = useState('');
  const [query, setQuery] = useState('');

  function onChange(e) {
    setText(e.target.value);               // জরুরি: বাক্সের অক্ষর
    startTransition(() => {
      setQuery(e.target.value);            // অ-জরুরি: বড় তালিকা
    });
  }
  return (<>
    <input value={text} onChange={onChange} />
    <SlowList q={query} />
  </>);
}

Transition-এর render time slicing-এ চলে: React-এর scheduler সোর্সে প্রতি টুকরো 5 ms ধরা আছে (frameYieldMs = 5), এরপর ব্রাউজারকে সময় ছেড়ে দেয়। ধরুন তালিকার কাজ 200 ms. একটানা চললে পুরো 200 ms ব্রাউজার আটকে থাকে। টুকরো করলে 200 ÷ 5 = ৪০টি টুকরো, আর প্রতিটির ফাঁকে একটি ক্লিক বা অক্ষর ঢুকতে পারে। SlowList-কে memo দিয়ে মুড়ে রাখতে হয়, নইলে জরুরি render-এও তালিকাটি চলবে।

আমরা এটি বাস্তবে মেপেছি। ১০০টি component-এর প্রতিটি ইচ্ছা করে 2 ms সময় নেয়। আসল কী-চাপে “abcde” টাইপ করা হলো, প্রতিটি অক্ষরের ফাঁক 60 ms.

সত্যিকারের run: React 19.3.1, Vite 8.3.5, development build, headless Edge 156. এটি একটি যন্ত্রের একটি স্থানীয় মাপ, সাধারণ ফল নয়।

blocking   slowRenders 500 listCommits 5 longtasks ms [207, 203, 203, 205, 203] value abcde
transition slowRenders 252 listCommits 1 longtasks ms [] value abcde

Transition ছাড়া ৫টি অক্ষরে ৫ বার পুরো তালিকা চলেছে (৫ × ১০০ = ৫০০), আর প্রতিবার প্রায় 205 ms-এর একটি long task তৈরি হয়েছে। Transition-এ long task শূন্য, তালিকা commit হয়েছে মাত্র ১ বার। ২৫২ থেকে শেষ ১০০টি বাদ দিলে বাকি ১৫২টি render শুরু হয়েও বাতিল হয়েছে, DOM-এ যায়নি। তিনবার চালিয়ে সংখ্যাটি পেয়েছি ২৫২, ২৫৮ আর ২৪০. বিনিময়ে তালিকা একটু দেরিতে আসে, কিন্তু টাইপিং আটকায় না। INP-এর মতো সাড়া দেওয়ার মাপ এখানেই উপকার পায়।

একই পরিবারের আরও দুটি: useDeferredValue একটি মানের পুরোনো কপি কিছুক্ষণ ধরে রাখে; props থেকে পাওয়া মানের জন্য এটি সুবিধাজনক। Suspense ডেটা বা কোড আসার আগে fallback দেখায়। বিস্তারিত পর্ব ২৩-এ।

৭ · Batching আর “re-render মানেই DOM বদল নয়”

একটি click handler-এ তিনটি set ডাকলে কি তিনবার render হয়? না। Batching-এর কারণে React সব update জমিয়ে একবারই render করে। ওয়েটার প্রতিটি পদের জন্য আলাদা করে রান্নাঘরে দৌড়ান না; অর্ডার শেষ করে একবারে যান। React 18-এর আগে এটি শুধু React-এর event handler-এ হতো। React 18 থেকে setTimeout, promise বা native event-এও স্বয়ংক্রিয়ভাবে হয়।

এবার মাপার পালা। নিচের কোডে flushSync দিয়ে প্রতিটি update-কে সঙ্গে সঙ্গে শেষ করা হয়েছে, যাতে এক এক ধাপের হিসাব আলাদা পাওয়া যায়। সাধারণ অ্যাপে এটি লাগে না। MutationObserver আসল DOM বদলের রেকর্ড গোনে, আর renders গোনে Row ফাংশন কতবার চলল।

<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const items = ['ডাল', 'চাল', 'তেল', 'চিনি'].map((name, i) => ({ id: i, name, price: 100 + i }));
let renders = 0, setList;
function Row({ p }) { renders++; return <li>{p.name}: ৳{p.price}</li>; }
function Shop() {
  const [list, set] = useState(items);
  setList = set;
  return <ul>{list.map(p => <Row key={p.id} p={p} />)}</ul>;
}
const host = document.getElementById('root');
const root = createRoot(host);
ReactDOM.flushSync(() => root.render(<Shop />));
const mo = new MutationObserver(() => {});
mo.observe(host, { subtree: true, childList: true, characterData: true, attributes: true });
function step(label, fn) {
  renders = 0;
  ReactDOM.flushSync(fn);
  console.log(label, '| render:', renders, '| DOM বদল:', mo.takeRecords().length);
}
const copy = l => [...l];
step('একই তথ্য, নতুন array', () => setList(copy));
step('১টি দাম বদল', () => setList(l => l.map(p => p.id === 1 ? { ...p, price: 99 } : p)));
step('তিনবার set', () => { setList(copy); setList(copy); setList(copy); });
step('শেষেরটি সামনে', () => setList(l => [l[3], l[0], l[1], l[2]]));
</script>

আউটপুট:

একই তথ্য, নতুন array | render: 4 | DOM বদল: 0
১টি দাম বদল | render: 4 | DOM বদল: 1
তিনবার set | render: 4 | DOM বদল: 0
শেষেরটি সামনে | render: 4 | DOM বদল: 6

প্রথম লাইনটিই এই পর্বের সবচেয়ে বড় কথা: ৪টি render, অথচ DOM-এ শূন্য বদল। দ্বিতীয় লাইনে ৪টি ফাংশন চললেও DOM-এ গেল শুধু ১টি লেখা। তৃতীয় লাইনে তিনবার set ডেকেও render ১২ নয়, ৪টি; এটিই batching. চতুর্থ লাইনে ৬টি রেকর্ড, কারণ আগের মতোই ৩টি node সরেছে, আর MutationObserver প্রতিটি সরানোকে একটি মোছা আর একটি যোগ, মোট ২টি রেকর্ডে দেখায়।

মূল ধারণা

“Re-render” আর “DOM update” দুটি আলাদা ঘটনা। React-এ অপচয় বলতে সাধারণত ফাংশন চালানো আর তুলনার খরচ বোঝায়, DOM-এর খরচ নয়। তাই “এই component re-render হচ্ছে” দেখেই আতঙ্কিত হবেন না; আগে মাপুন, সেটি ধীর কি না।

৮ · Profiler-এ render আর commit আলাদা করে দেখা

হাতে গোনা বাদ দিয়ে এবার আসল টুলে আসি। React-এর <Profiler> component প্রতিটি commit-এর পর আপনার ফাংশন ডাকে। React DevTools-এর Profiler ট্যাবও একই ধরনের সময় দেখায়। “বাজারঘর”-এর ১,০০০ সারির তালিকায় একটি পণ্যের দাম বদলে মাপলাম, memo ছাড়া আর সহ।

const onRender = (id, phase, actualDuration) => {
  console.log(id, phase, actualDuration.toFixed(2) + ' ms');
};

<Profiler id="shop" onRender={onRender}>
  <Shop />
</Profiler>

সত্যিকারের run: React 19.3.1, Vite 8.3.5 development build, headless Edge 156. প্রথমে ৬০ বার update দিয়ে JavaScript ইঞ্জিন গরম করা, তারপর ৪১টি update-এর মধ্যমা। domCalls হলো insertBefore, appendChild, removeChild আর nodeValue-এর ডাক গুনে পাওয়া সংখ্যা। একটি স্থানীয় মাপ, সাধারণ ফল নয়; production build-এ সময় অন্যরকম হবে।

nomemo mount: rows=1000 domCalls=4001 actual=25.8
nomemo update median of 41: rows=1000 domCalls=1 actual=2.70ms
memo   mount: rows=1000 domCalls=4001 actual=24.5
memo   update median of 41: rows=1 domCalls=1 actual=1.20ms

দেখুন কী পাওয়া গেল। Mount-এ domCalls ৪,০০১টি: ১,০০০টি সারির প্রতিটিতে ৪টি (li আর ৩টি লেখা), সাথে একটি ul. Update-এ memo ছাড়া ১,০০০টি সারি render হলো, কিন্তু DOM-এ গেল ঠিক ১টি কাজ। memo-তে render ১টিতে নামল, DOM কাজ একই ১টি, আর সময় প্রায় 2.7 ms থেকে 1.2 ms হলো। অর্থাৎ memo render বাঁচিয়েছে, কিন্তু ১,০০০ গুণ সময় বাঁচায়নি, কারণ ১,০০০টি props তুলনা তখনো চলছে। আর এখানে 2.7 ms নিজেই ফ্রেম বাজেটের অনেক ভেতরে। এমন তালিকায় memo না দিলেও ব্যবহারকারী পার্থক্য টের পেতেন না।

React DevTools-এ যা দেখবেন (বর্ণনা, এই পর্বের জন্য extension চালানো হয়নি; নাম version অনুযায়ী বদলাতে পারে):

  • ব্রাউজারে React Developer Tools extension বসিয়ে development build খুলুন। Profiler ট্যাবে রেকর্ড শুরু করে অ্যাপে কাজ করুন, তারপর থামান।
  • ওপরের ডানদিকে ছোট ছোট দণ্ড: প্রতিটি দণ্ড একটি commit. উঁচু দণ্ড মানে বেশি সময়। “৫টি ক্লিকে ৫টি দণ্ড” দেখলে batching-এর হিসাব মিলিয়ে নিতে পারবেন।
  • Flamegraph-এ প্রতিটি component-এর সময়; ধূসর component সেই commit-এ render হয়নি। Ranked ভিউ সবচেয়ে ধীরগুলো ওপরে দেখায়।
  • সেটিংস থেকে “কেন render হলো” রেকর্ড চালু করলে বদলানো props বা state জানা যায়।

Chrome-এর Performance প্যানেলেও সাম্প্রতিক React-এ Performance Tracks দেখা যায়: Scheduler track-এ Blocking, Transition, Suspense, Idle সারি, আর প্রতিটি কাজে Update, Render, Commit, Remaining Effects অংশ। এগুলো development build-এ চালু থাকে। সাধারণ production build-এ <Profiler>-এর onRender ডাকা হয় না; আলাদা profiling build লাগে।

৯ · Solid আর Svelte: virtual DOM ছাড়া পথ

React-এর পথ হলো “আবার চালানো, তারপর মেলানো।” কিন্তু যদি আগে থেকেই জানা যায়, কোন মান কোন DOM জায়গায় বসে? তাহলে মেলানোর দরকারই নেই। এই ভাবনাই fine-grained reactivity.

ReactSolidSvelte 5
State বদলালেComponent ফাংশন আবার চলেSignal শুধু তার পাঠকদের খবর দেয়Compiler-এর লেখা কোড নির্দিষ্ট জায়গা বদলায়
Component ফাংশনপ্রতি render-এ চলেএকবারই চলেসেটআপ একবার চলে
DiffTree মিলিয়ে দেখেসাধারণত দরকার হয় নাসাধারণত দরকার হয় না (list-এ key এখনো লাগে)
মূল সুবিধাসাধারণ JavaScript মডেল, বিশাল ecosystemসরাসরি updateBuild-এ কাজ সরিয়ে নেওয়া
মূল দামRender আর তুলনার খরচReactivity-র নিজস্ব নিয়ম শিখতে হয়Compiler-নির্ভরতা, তুলনায় ছোট ecosystem

এর মানে “React পুরোনো” নয়। React Compiler (স্থিতিশীল ১.০, অক্টোবর ২০২৫) build-এর সময় স্বয়ংক্রিয়ভাবে memoization যোগ করে, ফলে অপ্রয়োজনীয় render কমে; virtual DOM মডেল কিন্তু থাকে। Benchmark-এ fine-grained পদ্ধতি অনেক ক্ষেত্রে এগিয়ে থাকলেও বেশিরভাগ অ্যাপে ধীরতার কারণ network, ছবি আর বড় bundle. Svelte নিয়ে পর্ব ৬৭-এ, Vue-র reactivity নিয়ে পর্ব ৬৬-এ আরও দেখবেন।

আরও গভীরে: Tree edit distance-এর ওপর Philip Bille-এর ২০০৫ সালের সমীক্ষা পরিচিত, আর Demaine ও সহকর্মীদের ২০০৯ সালের কাজে ordered tree-র জন্য O(n³) পদ্ধতি পাওয়া যায়। React-এর reconcileChildrenArray প্রথমে দুই list একসাথে হাঁটে, মিল না পেলে বাকিগুলো একটি Map-এ রাখে; সোর্সের মন্তব্যই বলে, এটি reversal দুই-প্রান্ত-খোঁজার মতো ভালো সামলায় না। Lane হলো ৩১-bit-এর একটি bitmask. আরেকটি সূক্ষ্মতা: সোর্সে লেখা আছে default priority update চলমান transition-কে থামায় না। আমাদের প্রথম পরীক্ষায় setTimeout থেকে update পাঠিয়ে বাধা দেখা যায়নি; আসল কী-চাপে দেখা গেল।

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

  • Virtual DOM কী? মেমরিতে UI-এর object-বর্ণনা, যা আসল DOM-এর সাথে মেলানো হয়।
  • React-এর diff O(n) কেন? ভিন্ন ধরন মানে পুরো subtree বদল, আর key দিয়ে list মেলানো।
  • Index key সমস্যা করে কখন? ক্রম বদলালে বা মাঝে যোগ-বিয়োগ হলে, বিশেষ করে উপাদানে state থাকলে।
  • Render আর commit-এর পার্থক্য? Render হিসাব করে ও থামতে পারে; commit DOM বদলায় ও একটানা চলে।
  • Automatic batching কী? React 18 থেকে একই tick-এর একাধিক update একবার render হয়।

১০ · AI-এর যুগে: performance পরামর্শ যাচাই করা

AI-কে “React অ্যাপ দ্রুত করে দাও” বললে সে প্রায়ই সব জায়গায় memo আর useMemo ছড়িয়ে দেয়, না মেপেই। এই পর্বের জ্ঞান দিয়ে আপনি তার প্রতিটি দাবি পরীক্ষা করতে পারেন।

AI যা প্রায়ই করেসমস্যাযাচাই
key={index} বা key={Math.random()}State ভুল জায়গায় বা প্রতিবার ধ্বংসReorder করে state দেখুন
“Virtual DOM DOM-এর চেয়ে দ্রুত” বলাভুল ধারণা, ভুল সিদ্ধান্তRender আর DOM বদল আলাদা করে মাপুন
পুরোনো ReactDOM.render বা class lifecycle দেওয়াReact 19-এ ReactDOM.render সরানো হয়েছেআপনার version-এর react.dev দেখুন
অস্তিত্বহীন option বা package বানানোInstall বা build ভাঙে, অথবা অচেনা package ঢোকেnpm পাতা আর official docs মিলিয়ে দেখুন
Component-এর ভেতরে component লেখাপ্রতি render-এ state হারায়Input-এ টাইপ করে focus দেখুন
startTransition ঢুকিয়ে সব সমাধান দাবি করাControlled input-এ এটি চলে নাআসল ইনপুটে long task মাপুন

AI-কে অনুমান নয়, প্রমাণসহ বিশ্লেষণ করতে বলুন। শেষ শর্তটি খেয়াল করুন: মাপ ছাড়া কোনো বদল নয়।

আমি React 19 ব্যবহার করি। নিচের component-এ list আপডেটে ধীরতা লাগছে। [component-এর কোড] ১. কোন component কেন re-render হচ্ছে, আর এর কোনটি আসল DOM বদলায়? প্রতিটি দাবির কারণ বলো। ২. key কি স্থায়ী? state কোথায় হারাতে পারে? ৩. React DevTools Profiler-এ আমি কী মাপলে তোমার অনুমান যাচাই হবে, ধাপে ধাপে বলো। ৪. মাপার আগে কোনো memo বা useMemo যোগ করো না। শুধু যে লাইন বদলাতে বলছ সেটি দেখাও, আর react.dev-এর কোন পাতায় এই API আছে সেটি বলো।

AI-এর পরামর্শ নেওয়ার আগে চেকলিস্ট

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

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

প্র ০১“Virtual DOM আসল DOM-এর চেয়ে দ্রুত” — কথাটি কেন ছড়াল, আর সঠিক বক্তব্য কী?

২০১৩ সালে React আসার সময় তুলনাটি হচ্ছিল তখনকার পরিচিত পদ্ধতির সাথে। অনেক template library মডেল বদলালেই বড় অংশের innerHTML নতুন করে বসাত। এতে পুরোনো node মুছে নতুন node তৈরি হতো, focus আর scroll হারাত, আর ব্রাউজারকে বড় অংশ নতুন করে হিসাব করতে হতো। আরেক দিকে ছিল dirty checking বা নানা ধরনের data-binding, যেগুলো বড় অ্যাপে কখনো কখনো ধীর হয়ে পড়ত। সেই তুলনায় React-এর “শুধু পার্থক্যটুকু বদলাও” সত্যিই ভালো ছিল। তুলনার প্রেক্ষাপট হারিয়ে গিয়ে স্লোগান দাঁড়াল “Virtual DOM দ্রুত”।

কিন্তু যুক্তিতে দেখুন। একটি দাম বদলাতে হাতের কোড লেখে priceEl.textContent = '৳110'. React-ও শেষে প্রায় এই কাজটিই করে, তবে আগে component চালায় আর tree মেলায়। তাই সর্বোচ্চ সম্ভাব্য গতিতে হাতের সুনির্দিষ্ট কোড কখনো হারবে না। Rich Harris ২০১৮ সালের লেখায় ঠিক এই কথাটিই জোর দিয়ে বলেন: virtual DOM-এর কাজগুলো শেষ DOM কাজের অতিরিক্ত।

তাহলে React-এর দল কেন এটি বেছে নিল? কারণ প্রশ্নটি ছিল প্রকৌশলের, প্রতিযোগিতার নয়। বড় অ্যাপে প্রতিটি জায়গা হাতে মেলানো বাগের উৎস। React এক নির্দিষ্ট, অনুমানযোগ্য খরচের বিনিময়ে “UI হলো state-এর ফাংশন” মডেল দেয়। মূল্যটি আসে এই মডেল থেকে, স্লোগানের গতি থেকে নয়।

মূল উপলব্ধি: সঠিক বক্তব্য হলো, “Virtual DOM বেশিরভাগ বাস্তব কাজে যথেষ্ট দ্রুত, আর বিনিময়ে declarative কোড লেখার সুবিধা দেয়।” কোনো প্রযুক্তিকে “দ্রুত” বলার আগে জিজ্ঞেস করুন: কার তুলনায়, কোন কাজে, আর কীভাবে মাপা হয়েছে?

প্র ০২React-এর O(n) diff নিখুঁত নয়। কোথায় এটি বেশি কাজ করে, আর এই ছাড় কি যুক্তিসংগত?

Heuristic মানেই কিছু ক্ষেত্রে অদক্ষ উত্তর। ধরুন একটি বড় তালিকা <div>-এ ছিল। কোনো কারণে সেটি <section> হয়ে গেল, ভেতরের ১,০০০টি সারি হুবহু একই। নিখুঁত algorithm বলত, “শুধু বাইরের ট্যাগটি বদলান।” React পুরো subtree ফেলে ১,০০০টি সারি নতুন করে বানায়। State হারায়, input-এর focus যায়, effect-এর cleanup চলে আর নতুন করে mount হয়।

একইভাবে, সমতল কোনো উপাদান এক স্তর নিচে নেমে গেলে (<Row/> হঠাৎ <div><Row/></div>) React সেটিকে সরানো হিসেবে চেনে না, কারণ সে শুধু একই স্তরে তুলনা করে। Key-ও শুধু ভাইবোনদের মধ্যে কাজ করে; এক parent থেকে অন্য parent-এ গেলে state থাকে না। আর আমরা দেখেছি, শেষের উপাদান সামনে আনলে বাকি সবাই move হয়।

তবু ছাড়টি কেন যুক্তিসংগত? প্রথমত, এই ঘটনাগুলো বাস্তবে কম ঘটে। Update-এর বড় অংশে tree-র কাঠামো একই থাকে, শুধু লেখা বা attribute বদলায়। দ্বিতীয়ত, O(n³)-এর দাম সব update-এ দিতে হতো, আর heuristic-এর ক্ষতি হয় কালেভদ্রে। তৃতীয়ত, নিয়ম সহজ হওয়ায় আচরণ অনুমান করা যায়: “ধরন বদলালে state যায়।” Tree edit distance-এর জটিল উত্তর থেকে এমন স্পষ্ট নিয়ম পাওয়া যেত না, আর state কোথায় যাবে তা বলা কঠিন হতো।

মূল উপলব্ধি: প্রকৌশলে “সবসময় নিখুঁত, কিন্তু ধীর” প্রায়ই “প্রায় সবসময় যথেষ্ট ভালো, দ্রুত আর অনুমানযোগ্য”-এর কাছে হারে। Heuristic ব্যবহার করলে তার সীমাটিও জানতে হয়, যাতে সীমার বাইরে গেলে আপনি অবাক না হন।

প্র ০৩React কেন নিজের core নতুন করে লিখল? Fiber থেকে concurrent rendering আসতে এত বছর লাগল কেন?

পুরোনো reconciler (পরে যাকে “stack reconciler” বলা হয়) recursion-এ চলত। JavaScript engine-এর call stack React-এর নিয়ন্ত্রণে নেই। একবার গভীরে ঢুকলে শেষ না হওয়া পর্যন্ত ফেরা যায় না। ছোট অ্যাপে এটি সমস্যা ছিল না, কিন্তু বড় tree, animation আর টাইপিংয়ের সময় ফ্রেম পড়ে যেত।

Fiber-এর মূল চিন্তা হলো stack-কে নিজেই heap-এ পুনর্গঠন করা। Andrew Clark-এর পরিচিত নোটে বলা হয়েছিল, একটি fiber হলো একটি “virtual stack frame”, আর লক্ষ্য চারটি: কাজ থামিয়ে পরে ফেরা, কাজে অগ্রাধিকার দেওয়া, আগের কাজ পুনর্ব্যবহার করা, আর অপ্রয়োজনীয় কাজ বাতিল করা। ২০১৭ সালের সেপ্টেম্বরে React 16-এ এই নতুন core প্রকাশ পায়; ঘোষণায় নিজেরাই মজা করে লিখেছিল, “we rewrote React!” কিন্তু মজার বিষয়, তখন async rendering চালু করা হয়নি, যাতে পুরোনো অ্যাপ নির্বিঘ্নে উঠতে পারে।

Concurrent ক্ষমতা ব্যবহারে আসে ২০২২ সালে React 18-এ, প্রায় পাঁচ বছর পরে। কেন? প্রযুক্তিগত ভিত্তি থাকলেও সবচেয়ে কঠিন ছিল পুরোনো কোডের সাথে সামঞ্জস্য। থামানো যায় এমন render মানে আপনার ফাংশন একাধিকবার চলতে পারে। Render-এর ভেতরে side effect থাকলে তখন বাগ হবে। তাই component-কে pure রাখার নিয়ম কড়া হলো, StrictMode ফাংশন দুবার চালিয়ে ভুল ধরা শুরু করল, আর শেষে React দল “সব অ্যাপে চালু” না করে feature ধরে opt-in পথ নিল।

মূল উপলব্ধি: বড় rewrite-এর সবচেয়ে কঠিন অংশ নতুন কোড লেখা নয়, পুরোনো ব্যবহারকারীদের না ভেঙে তাদের নিয়ে আসা। একটি ক্ষমতা তৈরি করা আর সেটিকে নিরাপদে সবার হাতে দেওয়া, দুটি আলাদা প্রকল্প।

প্র ০৪Index key কি সবসময় ভুল? কোনো উপাদানে state না থাকলেও কি এটি ক্ষতি করে?

“কখনো index নয়” কথাটি সহজ নিয়ম হিসেবে কাজের, কিন্তু পুরোপুরি ঠিক নয়। React-ও key না পেলে পেছনে index-ই ব্যবহার করে আর সতর্কবার্তা দেয়। আসল প্রশ্ন হলো, এই list-এ একটি উপাদানের পরিচয় কি তার ক্রমের সাথে বাঁধা?

একটি স্থির list ভাবুন: ফুটারের পাঁচটি লিংক, যা কখনো বদলায় না। এখানে index একদম নিরাপদ, ক্রম বদলের প্রশ্নই নেই। আবার একটি pagination, যেখানে প্রতিটি পাতায় ১০টি সারি আর উপাদানে কোনো state নেই। এখানে index দিলে পাতা বদলে node পুনর্ব্যবহার হয়, শুধু লেখা বদলায়; id key দিলে সব node ধ্বংস হয়ে নতুন তৈরি হয়। এই বিশেষ ক্ষেত্রে index কাজের হিসাবে কম খরচের হতেও পারে।

কিন্তু উপাদানে যদি input, checkbox, animation, focus বা নিজের state থাকে, অথবা শুরুতে যোগ, মাঝে মোছা বা sort হয়, তাহলে index key সরাসরি বাগ তৈরি করে। “বাজারঘর”-এর কার্টে ডালের সংখ্যার ঘরে লেখা ৩ মুছে ফেলার পর সেটি চালের সারিতে দেখা যেতে পারে। এর চেয়ে খারাপ বিষয়, হয়তো কোনো error দেখাবে না, আর অর্ডারে ভুল পরিমাণ চলে যাবে। State না থাকলেও শুরুতে যোগ করলে ১,০০০টি অপ্রয়োজনীয় DOM লেখা হবে, যা আপনি কোডে গুনে দেখেছেন।

Math.random() আরও খারাপ, কারণ এটি index-এর মতো ভুল জায়গায় state রাখে না, বরং প্রতি render-এ সব state আর DOM ধ্বংস করে। ডেটার id না থাকলে আগেই (যেমন ডেটা তৈরির সময়) একটি স্থায়ী id বসিয়ে নিন, render-এর সময় নয়।

মূল উপলব্ধি: Key কোনো lint-এর ঝামেলা নয়, এটি পরিচয়ের ঘোষণা। জিজ্ঞেস করুন: “এই সারিটি কে?” উত্তর যদি “৩ নম্বর সারি” হয় আর ডেটায়ও সেটিই সত্য, তাহলে index চলে। উত্তর যদি “মসুর ডাল” হয়, তাহলে id দিন।

প্র ০৫Solid বা Svelte benchmark-এ এগিয়ে থাকলে সবাই React ছাড়ছে না কেন? আপনি কীভাবে সিদ্ধান্ত নেবেন?

Fine-grained পদ্ধতির যুক্তি শক্ত। State বদলালে পুরো component আবার চালানোর বদলে সরাসরি নির্দিষ্ট জায়গা বদলায়। Solid-এর ডকুমেন্টেশন বলে, component ফাংশন মাত্র একবার চলে। Svelte build-এর সময় কাজ সরিয়ে নেওয়ার ধারণাটি জনপ্রিয় করে, আর Svelte 5-এ runes ($state) দিয়ে state ঘোষণার নতুন পথ আনে। Vue, Angular-এও signal-ধরনের ভাবনা জায়গা পেয়েছে।

তবু framework বাছাইয়ে বিশুদ্ধ update গতি একটি উপাদান মাত্র। প্রথমত ecosystem: component library, form library, চাকরির বাজার, শিক্ষার উপকরণ আর অভিজ্ঞ লোক পাওয়া। React-এর বিশাল ecosystem একটি দলের জন্য বাস্তব সুবিধা। দ্বিতীয়ত, বেশিরভাগ ব্যবসায়িক অ্যাপে ধীরতার কারণ framework নয়, বরং ভারী bundle, বড় ছবি, ধীর API আর অপ্রয়োজনীয় third-party script. Benchmark সাধারণত হাজার হাজার সারি বদলানোর মতো চরম কাজ মাপে, যা আপনার অ্যাপ হয়তো করেই না। তৃতীয়ত, React নিজেও এগোচ্ছে: ২০২৫ সালে React Compiler স্থিতিশীল হয়েছে, যা হাতে memo লেখার দরকার কমায়।

অন্যদিকে সিদ্ধান্তের উল্টো দিকও আছে। কম-ক্ষমতার ফোনে চলা, অনেক ডেটা-ঘন dashboard, বা JavaScript খুব ছোট রাখতে হবে এমন অ্যাপে এই পার্থক্য অর্থবহ হতে পারে। বাংলাদেশের মতো বাজারে যেখানে অনেকে মাঝারি ফোন আর সীমিত data ব্যবহার করেন, সেখানে bundle আকার আর CPU খরচ উপেক্ষা করার মতো নয়।

সিদ্ধান্তের উপায় হলো প্রকৃত কাজ দিয়ে পরীক্ষা: আপনার আসল পাতার মতো একটি ছোট নমুনা বানিয়ে, লক্ষ্য ব্যবহারকারীর মতো ধীর ফোনে INP আর bundle আকার মাপুন। সাথে হিসাব করুন দলের দক্ষতা আর নিয়োগ।

মূল উপলব্ধি: Benchmark একটি প্রশ্নের উত্তর দেয়, আর stack বাছাইয়ের প্রশ্ন অনেকগুলো। গতি একটি বৈশিষ্ট্য; দল, ecosystem আর রক্ষণাবেক্ষণও তাই। পর্ব ৮৫-এ এই সিদ্ধান্তের ছকটি পাবেন।

অনুশীলন

প্রথমে নিজে করুন, তারপর উত্তর মিলিয়ে নিন।

  1. হিসাব (numerical): একটি পাতায় ৫,০০০ node আছে। (ক) O(n³) আর O(n) তুলনার সংখ্যা কত? (খ) সেকেন্ডে 10⁸ তুলনা ধরে দুটির সময় কত? (গ) ধরুন একটি transition-এর render-এ ৫,০০০টি component-এর প্রতিটি 0.02 ms নেয়। মোট সময় কত, আর 5 ms টুকরোয় কয়টি টুকরো লাগবে? এই কাজ এক টানে চললে কি long task হবে?

    (ক) n³ = (5×10³)³ = 125×10⁹ = ১২,৫০০ কোটি। n = ৫,০০০।

    (খ) 1.25×10¹¹ ÷ 10⁸ = 1,250 s, মানে প্রায় ২১ মিনিট। আর 5×10³ ÷ 10⁸ = 5×10⁻⁵ s = 50 µs.

    (গ) 5,000 × 0.02 ms = 100 ms. 100 ÷ 5 = ২০টি টুকরো। এক টানে চললে 100 ms > 50 ms, তাই long task হবে। টুকরো করলে প্রতিটি 5 ms, কোনো long task নয়; ফাঁকে ব্রাউজার সাড়া দিতে পারে। মোট কাজ কিন্তু কমেনি।

  2. ফল আগে বলুন (predict the output): keyed-vs-unkeyed.js-এর নিয়মে key-সহ [A,B,C,D,E] থেকে (ক) [E,A,B,C,D] আর (খ) [B,C,D,E,A]-এ কয়টি move হবে? (গ) প্রথম list-এ A-র ভেতরের state 5 হলে, key=নাম আর key=index দুই ক্ষেত্রে (ক)-এর পর প্রথম সারিতে কোন নাম আর কোন state দেখবেন? আগে লিখে তারপর কোডে show() যোগ করে মিলিয়ে নিন।

    (ক) ৪টি move. E-র পুরোনো index 4, তাই lastPlacedIndex = 4. এরপর A, B, C, D-র সবার পুরোনো index 4-এর ছোট, সবাই সরবে।

    (খ) ১টি move. B=1, C=2, D=3, E=4 ক্রমশ বাড়ছে, তাই সবাই জায়গায় থাকে। শেষে A-র পুরোনো index 0 < 4, শুধু A সরে।

    (গ) Key=নাম হলে প্রথম সারিতে E, state 0 (A-র 5 তার সাথে সরে দ্বিতীয় সারিতে যায়)। Key=index হলে প্রথম সারিতে নাম E, কিন্তু state 5: জায়গার state জায়গাতেই রয়ে গেছে, এটিই বাগ।

  3. Code review (critique): একজন সহকর্মী নিচের কোড দিয়েছেন। অভিযোগ: প্রতিটি অক্ষর টাইপের পর input-এর focus চলে যায়, আর সার্চ list ধীর। অন্তত তিনটি সমস্যা ধরুন আর নকশা ঠিক করুন।
    function Page({ items }) {
      const [q, setQ] = useState('');
      function Box() {
        return <input value={q} onChange={e => setQ(e.target.value)} />;
      }
      return (<>
        <Box />
        {items.filter(x => x.name.includes(q)).map(x =>
          <Row key={Math.random()} item={x} />)}
      </>);
    }

    ১. Box ভেতরে সংজ্ঞায়িত, তাই প্রতি render-এ নতুন ধরন। Input ধ্বংস হয়, focus যায়। সমাধান: Box বাইরে আনুন, অথবা সরাসরি <input> লিখুন।

    ২. key={Math.random()} প্রতি অক্ষরে সব Row ধ্বংস করে নতুন বানায়, এটিই ধীরতার বড় কারণ হতে পারে। সমাধান: key={x.id}.

    ৩. ফিল্টার হয় প্রতিটি অক্ষরে। আগে ১ আর ২ ঠিক করে Profiler-এ মাপুন। এরপরও ধীর হলে Row-কে memo করে দেখা, আর list-এর জন্য useDeferredValue বা transition ব্যবহার করা যায়। Input-এর নিজের state জরুরি থাকবে। বিশাল list-এ windowing-ও একটি পথ।

  4. নিজে মাপুন: renders-vs-dom.html ব্লকে Row-কে memo(Row) দিয়ে মুড়ে আবার চালান: const Row = memo(function ({ p }) { … }); লিখুন। প্রতিটি লাইনের render সংখ্যা কত হবে? DOM বদল কি বদলাবে? আগে অনুমান লিখুন।

    আমরা আসল React-এ চালিয়ে এই ফল পেয়েছি:

    একই তথ্য, নতুন array | render: 0 | DOM বদল: 0
    ১টি দাম বদল | render: 1 | DOM বদল: 1
    তিনবার set | render: 0 | DOM বদল: 0
    শেষেরটি সামনে | render: 0 | DOM বদল: 6

    p object-গুলো একই reference, তাই memo render এড়িয়েছে। একটি দামের object নতুন, তাই শুধু সেই সারি render হয়েছে। DOM-এর সংখ্যা একটিও বদলায়নি। শেষ লাইনটি দেখার মতো: render শূন্য, অথচ DOM বদল ৬. সরানোর কাজ করে parent-এর reconciliation, সারির ফাংশন চলার দরকারই হয় না।

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

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

ক্রমএকটি ক্লিক থেকে পর্দা বদল পর্যন্ত ধাপগুলো সাজান (▲▼ চেপে সরান)

  1. Event handler-এ setState ডাকা
  2. Component ফাংশন চলে, fiber-গুলোয় diff হয়
  3. Commit-এ DOM বদলায়
  4. useLayoutEffect চলে
  5. ব্রাউজার পর্দায় আঁকে (paint)

React-এর virtual DOM সম্পর্কে কোন বক্তব্যটি সঠিক?

  • এটি আসল DOM-এর চেয়ে দ্রুত, তাই হাতের DOM কোডকে সবসময় হারায়
  • এটি UI-এর object-বর্ণনা, যা মিলিয়ে DOM-এর দরকারি বদল ঠিক করা হয়
  • এটি ব্রাউজারের ভেতরে থাকা একটি দ্বিতীয় DOM
  • এটি থাকলে DOM বদলানোর আর দরকার হয় না

React-এ একই জায়গায় <div><Counter/></div> থেকে <section><Counter/></section> হলো। Counter-এর state-এর কী হবে?

  • থাকবে, কারণ Counter একই component
  • শুধু key থাকলে হারাবে, নইলে থাকবে
  • হারাবে, কারণ পুরো subtree ধ্বংস হয়ে নতুন তৈরি হয়
  • React একটি error দেখাবে

স্থায়ী key-সহ list [A,B,C,D] থেকে [D,A,B,C] হলো। React-এর সামনে-থেকে-হাঁটা lastPlacedIndex নিয়মে কয়টি node সরবে?

  • ৩টি: A, B, C
  • ১টি: শুধু D
  • ৪টি: সবগুলো
  • ০টি

React-এর render আর commit ধাপ সম্পর্কে কোনটি ঠিক?

  • Render ধাপে DOM বদলায়, commit ধাপে শুধু হিসাব হয়
  • দুটি ধাপই মাঝপথে থামানো যায়
  • Render ধাপ প্রতিটি update-এ ঠিক একবারই চলে, এটি নিশ্চিত
  • Render থামতে বা বাতিল হতে পারে; commit DOM বদলায় ও একটানা চলে

React 19-এ একটি setTimeout-এর ভেতরে পরপর দুটি state update ডাকা হলো। সাধারণ অবস্থায় কতবার render হবে?

  • দুবার, কারণ এটি React-এর event handler নয়
  • একবার, automatic batching-এর কারণে
  • একবারও না
  • প্রতি ফ্রেমে একবার করে

১,০০০ সারির একটি তালিকায় memo ছাড়া একটি পণ্যের দাম বদলালে সাধারণত কী ঘটে?

  • ১,০০০টি সারি render হয় আর ১,০০০টি DOM node বদলায়
  • শুধু ১টি সারি render হয়, বাকি ৯৯৯টির ফাংশন চলে না
  • ১,০০০টি সারির ফাংশন চলে, কিন্তু DOM-এ প্রায় ১টি বদলই যায়
  • পুরো তালিকা DOM থেকে মুছে নতুন করে তৈরি হয়

startTransition-এ রাখা একটি বড় তালিকার render চলছে, এমন সময় ব্যবহারকারী একটি অক্ষর টাইপ করলেন। React সাধারণত কী করে?

  • চলমান render থামিয়ে অক্ষরের update আগে সারে, তালিকা পরে নতুন করে বানায়
  • তালিকার render শেষ না হওয়া পর্যন্ত অক্ষর দেখায় না
  • দুটি কাজ দুটি CPU thread-এ একসাথে চালায়
  • আধা-বানানো তালিকা সাথে সাথে DOM-এ বসিয়ে দেয়

শব্দকোষ

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

মনে রাখুন

  1. Virtual DOM হলো UI-এর object-বর্ণনা। এটি “দ্রুত” নয়, যথেষ্ট দ্রুত; এর মূল্য declarative মডেলে।
  2. Diff-এর দুটি নিয়ম: ধরন আলাদা হলে subtree নতুন, আর key পরিচয় দেয়। তাই কাজ O(n).
  3. State থাকে tree-র জায়গায়। ধরন, key বা জায়গা বদলালে state হারাতে পারে।
  4. Fiber কাজকে টুকরো করে। Render থামতে পারে, commit থামে না; DOM বদলায় commit-এ, পর্দা বদলায় paint-এ।
  5. Re-render মানেই DOM বদল নয়। Batching, memo বা transition, যেকোনো পদক্ষেপের আগে Profiler-এ মাপুন।

পরের পর্বে: এখন আপনি জানেন state কোথায় থাকে আর কীভাবে বদলায়। মডিউল ৩-এর শুরুতে নকশার প্রশ্ন: একটি বড় অ্যাপে কোন state local, কোনটি lifted, কোনটি global, আর কোনটি আসলে server-এর।

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

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