পাঠ ১০ · ৫৮-এর মধ্যে · মডিউল ৩
Home / Courses / Full-Stack Web Frameworks / ভার্চুয়াল DOM

ভার্চুয়াল DOM ও রিকনসিলিয়েশন

Virtual DOM and reconciliation
১৩ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ভার্চুয়াল DOM কেন প্রয়োজন — রিয়েল DOM ম্যানিপুলেশনের ব্যয় সম্পর্কে ধারণা
  • রিকনসিলিয়েশন ও ডিফিং-এর ধাপে-ধাপে প্রক্রিয়া
  • একটি সম্পূর্ণ কার্যকর diff(old_vnode, new_vnode) অ্যালগরিদম বাস্তবে লেখা
  • দুটি concrete vnode ট্রিতে diff চালিয়ে হাতে-হিসেব-করা প্যাচ লিস্টের সাথে মিলিয়ে যাচাই করা

১ · রিয়েল DOM কেন সরাসরি বারবার বদলানো ব্যয়বহুল

ব্রাউজারের রিয়েল DOM একটি জটিল, লাইভ কাঠামো — প্রতিটি এলিমেন্টের সাথে লেআউট (কোথায় বসবে), স্টাইল, ইভেন্ট লিসেনার ইত্যাদি জড়িত। DOM-এর একটি নোড পরিবর্তন করলে ব্রাউজারকে প্রায়ই পুরো লেআউট আবার হিসেব করতে হয় (রিফ্লোReflow / LayoutDOM পরিবর্তনের পর ব্রাউজার এলিমেন্টগুলোর অবস্থান ও আকার আবার হিসেব করে — এটি ব্যয়বহুল একটি অপারেশন।) এবং স্ক্রিনে আবার আঁকতে হয় (রিপেইন্ট)। একটি বড় UI-তে যদি প্রতিটি ছোট ডেটা পরিবর্তনে সরাসরি DOM নোড খোঁজা ও বদলানো হতো, তাহলে পারফরম্যান্স দ্রুত খারাপ হয়ে যেত।

সমাধান হিসেবে React, Vue-এর মতো ফ্রেমওয়ার্ক একটি ভার্চুয়াল DOM রাখে — L09-এ দেখা vnode ট্রির মতো একটি সাধারণ, হালকা ইন-মেমরি কাঠামো, যা রিয়েল DOM নোড নয়, শুধু "DOM কেমন হওয়া উচিত তার বর্ণনা"। যখন অ্যাপের স্টেট বদলায়, ফ্রেমওয়ার্ক প্রথমে একটি নতুন ভার্চুয়াল DOM ট্রি তৈরি করে (যা তৈরি করা সস্তা — শুধু Python/JS অবজেক্ট), তারপর সেটাকে পুরনো ভার্চুয়াল DOM ট্রির সাথে তুলনা করে বের করে ঠিক কোথায় আসলে পরিবর্তন দরকার — শুধু সেটুকুই রিয়েল DOM-এ প্রয়োগ করা হয়।

২ · রিকনসিলিয়েশন ও ডিফিং — প্রক্রিয়াটি কী

রিকনসিলিয়েশনReconciliationপুরনো ও নতুন ভার্চুয়াল DOM ট্রি তুলনা করে ন্যূনতম প্রয়োজনীয় রিয়েল-DOM পরিবর্তন বের করার প্রক্রিয়া। হলো পুরনো vnode ট্রি ও নতুন vnode ট্রি — দুটোকেই একসাথে, নোড-বাই-নোড হেঁটে তুলনা করার প্রক্রিয়া। এই তুলনাকে বলা হয় ডিফিং। ডিফিং অ্যালগরিদম প্রতিটি নোডের জন্য কয়েকটি প্রশ্নের উত্তর খোঁজে:

  • ট্যাগ কি বদলেছে? (যেমন div থেকে span) — হলে পুরো সাবট্রি প্রতিস্থাপন করতে হবে ("replace" প্যাচ)।
  • Props কি বদলেছে? — হলে শুধু বদলে যাওয়া props-গুলো আলাদাভাবে আপডেট করতে হবে ("update_props" প্যাচ), পুরো নোড না ভেঙে।
  • Children-এর সংখ্যা কি বদলেছে? — নতুন চাইল্ড যোগ হলে ("add_child"), বাদ গেলে ("remove_child")।
  • টেক্সট কনটেন্ট কি বদলেছে? — হলে সেই টেক্সট নোডটুকু প্রতিস্থাপন করতে হবে।

প্রতিটি "প্রশ্নের উত্তর" একটি ছোট প্যাচ অপারেশন হিসেবে একটি লিস্টে জমা হয় — ট্রির প্রতিটি নোডের একটি পাথ (root থেকে সেই নোড পর্যন্ত চাইল্ড-ইনডেক্সের ধারা, যেমন (1, 0) মানে root-এর চাইল্ড #1-এর চাইল্ড #0) দিয়ে চিহ্নিত করা হয়, যাতে পরে রিয়েল DOM-এ ঠিক কোন নোডে প্যাচটি প্রয়োগ করতে হবে তা জানা যায়।

পুরনো vnode ট্রি (before) নতুন vnode ট্রি (after) diff() নোড-বাই-নোড তুলনা প্যাচ লিস্ট [replace, update_props, ...] রিয়েল DOM-এ প্রয়োগ (শুধু বদলে যাওয়া অংশ)
পুরনো ও নতুন vnode ট্রি diff() ফাংশনে ঢোকে, যা একটি প্যাচ লিস্ট তৈরি করে — এই ন্যূনতম প্যাচগুলোই রিয়েল DOM-এ প্রয়োগ হয়, পুরো ট্রি আবার তৈরি হয় না।

৩ · সম্পূর্ণ diff() অ্যালগরিদম লেখা

নিচের কোড সেলে একটি সত্যিকারের, কার্যকর diff(old, new, path) ফাংশন লেখা হয়েছে যা L09-এর vnode কাঠামোর উপর কাজ করে (children হয় একটি vnode dict, নয়তো একটি প্লেইন স্ট্রিং — টেক্সট নোড)। এটি একটি concrete "টুডু-লিস্ট" এর আগে ও পরের অবস্থা তুলনা করে — যেখানে (ক) পুরো লিস্টের একটি নতুন প্রপ যোগ হয়েছে, (খ) একটি আইটেমের টেক্সট বদলেছে, এবং (গ) একটি নতুন আইটেম যোগ হয়েছে।

Python
# --- diff() অ্যালগরিদম ---
# একটি vnode হয় {"tag":..., "props":{...}, "children":[...]} অথবা একটি প্লেইন স্ট্রিং (টেক্সট নোড)

def diff(old, new, path=()):
    """পুরনো ও নতুন vnode তুলনা করে একটি প্যাচ-অপারেশন লিস্ট রিটার্ন করে।
    প্রতিটি প্যাচ একটি tuple: (op_name, path, ...extra_data)"""
    patches = []

    # কেস ১: নতুন নোড এমন জায়গায় যোগ হয়েছে যেখানে আগে কিছুই ছিল না
    if old is None and new is not None:
        patches.append(("add_child", path, new))
        return patches

    # কেস ২: পুরনো নোড আছে কিন্তু নতুন গাছে সেই জায়গায় কিছু নেই -- বাদ দিতে হবে
    if new is None and old is not None:
        patches.append(("remove_child", path))
        return patches

    # কেস ৩: দুটোর একটি টেক্সট নোড (স্ট্রিং) -- সরাসরি মান তুলনা
    if isinstance(old, str) or isinstance(new, str):
        if old != new:
            patches.append(("replace", path, new))
        return patches

    # কেস ৪: দুটোই এলিমেন্ট vnode -- ট্যাগ বদলেছে কি না দেখা
    if old["tag"] != new["tag"]:
        patches.append(("replace", path, new))
        return patches  # ট্যাগ বদলালে ভেতরের children আলাদাভাবে ডিফ করার দরকার নেই

    # কেস ৫: ট্যাগ একই -- props তুলনা করা (যোগ/পরিবর্তিত/বাদ-যাওয়া সব ধরা)
    old_props = old.get("props", {})
    new_props = new.get("props", {})
    changed_props = {}
    for key, value in new_props.items():
        if key not in old_props or old_props[key] != value:
            changed_props[key] = value
    for key in old_props:
        if key not in new_props:
            changed_props[key] = None  # None মানে এই প্রপ বাদ দিতে হবে
    if changed_props:
        patches.append(("update_props", path, changed_props))

    # কেস ৬: children রিকার্সিভভাবে ডিফ করা, ইনডেক্স অনুযায়ী মিলিয়ে
    old_children = old.get("children", [])
    new_children = new.get("children", [])
    max_len = max(len(old_children), len(new_children))
    for i in range(max_len):
        old_child = old_children[i] if i < len(old_children) else None
        new_child = new_children[i] if i < len(new_children) else None
        patches.extend(diff(old_child, new_child, path + (i,)))

    return patches


# --- concrete "আগে" ট্রি: দুই আইটেমের টুডু-লিস্ট ---
old_tree = {
    "tag": "ul",
    "props": {"class": "todo-list"},
    "children": [
        {"tag": "li", "props": {"class": "todo-item"}, "children": ["পানি কেনা"]},
        {"tag": "li", "props": {"class": "todo-item"}, "children": ["দুধ কেনা"]},
    ],
}

# --- concrete "পরে" ট্রি: একই লিস্ট, কিন্তু --
#   (১) ul-এ একটি নতুন প্রপ "data-count" যোগ হয়েছে
#   (২) দ্বিতীয় আইটেমের টেক্সট বদলেছে
#   (৩) তৃতীয় একটি নতুন আইটেম যোগ হয়েছে
new_tree = {
    "tag": "ul",
    "props": {"class": "todo-list", "data-count": "3"},
    "children": [
        {"tag": "li", "props": {"class": "todo-item"}, "children": ["পানি কেনা"]},
        {"tag": "li", "props": {"class": "todo-item"}, "children": ["দুধ ও ডিম কেনা"]},
        {"tag": "li", "props": {"class": "todo-item"}, "children": ["রুটি কেনা"]},
    ],
}

patch_list = diff(old_tree, new_tree)

print(f"মোট প্যাচ পাওয়া গেছে: {len(patch_list)}\n")
for i, patch in enumerate(patch_list, start=1):
    op = patch[0]
    path = patch[1]
    extra = patch[2] if len(patch) > 2 else None
    print(f"প্যাচ #{i}: op={op!r}  path={path}  data={extra}")

    

৪ · হাতে-হিসেব করে যাচাই করা

কোডটি Run করার আগে হাতে ট্রেস করে দেখা যাক তিনটি প্যাচ কেন সঠিক:

  • প্যাচ ১ — root-এ update_props: old_tree-এর ul-এ প্রপ ছিল শুধু {"class": "todo-list"}, new_tree-এ যোগ হয়েছে "data-count": "3" — path () (root), changed props {"data-count": "3"}।
  • প্যাচ ২ — path (1, 0)-এ replace: ইনডেক্স ১-এর li দুটোতেই একই ট্যাগ ও props, কিন্তু তার চাইল্ড #0 (টেক্সট) "দুধ কেনা" থেকে "দুধ ও ডিম কেনা"-তে বদলেছে — তাই path (1, 0) (root-এর চাইল্ড ১-এর চাইল্ড ০)।
  • প্যাচ ৩ — path (2,)-এ add_child: old_children-এর দৈর্ঘ্য ২ (ইনডেক্স ০,১), কিন্তু new_children-এর দৈর্ঘ্য ৩ — তাই ইনডেক্স ২-তে old_child হয় None, new_child হয় তৃতীয় li, ফলে add_child প্যাচ path (2,)-এ।

প্রথম আইটেম (ইনডেক্স ০, "পানি কেনা") সম্পূর্ণ অপরিবর্তিত থাকায় তার জন্য কোনো প্যাচ তৈরি হয়নি — এটাই ডিফিং-এর মূল লক্ষ্য: শুধু আসলে বদলে যাওয়া অংশ চিহ্নিত করা, পুরো ট্রি নয়।

লক্ষ্য করুন কোডে ট্যাগ ভিন্ন হলে (কেস ৪) সাথে সাথে return করা হয়েছে — children আর আলাদাভাবে ডিফ করা হয়নি। এটা ইচ্ছাকৃত: ট্যাগ বদলে গেলে (যেমন div থেকে span) ধরে নেওয়া হয় পুরো সাবট্রিটাই ভিন্ন, তাই পুরনো সাবট্রি ভেঙে ফেলে নতুনটা পুরোপুরি বসানোই স্বাভাবিক — ভেতরের প্রতিটি নোড আলাদাভাবে মেলানোর চেষ্টা করা অপ্রয়োজনীয় কাজ।
মূল কথা · Key takeaway

রিকনসিলিয়েশন পুরনো ও নতুন vnode ট্রি নোড-বাই-নোড তুলনা করে একটি ন্যূনতম প্যাচ লিস্ট তৈরি করে — শুধু ট্যাগ পরিবর্তন, props পরিবর্তন, ও children যোগ/বাদ হওয়া অংশগুলোই চিহ্নিত হয়, অপরিবর্তিত অংশ স্পর্শ করা হয় না। এই প্যাচ লিস্টই পরে রিয়েল DOM-এ প্রয়োগ করা হয় — ফলে ব্যয়বহুল পুরো-ট্রি রিরেন্ডারের বদলে শুধু প্রকৃত পরিবর্তনটুকু ঘটে, যা React/Vue-এর পারফরম্যান্সের কেন্দ্রীয় রহস্য।

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

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ কোডে path প্যারামিটারটি কেন প্রয়োজন — শুধু কোন নোড বদলেছে জানলেই কি যথেষ্ট নয়?

শুধু "কোন নোড বদলেছে" জানলে যথেষ্ট নয় — রিয়েল DOM-এ প্যাচ প্রয়োগ করার সময় ঠিক কোথায় (ট্রির কোন অবস্থানে) সেই নোডটি আছে তা জানতে হবে। path হলো root থেকে সেই নোড পর্যন্ত চাইল্ড-ইনডেক্সের একটি ধারা (যেমন (1, 0)) — এটি একটি ঠিকানার মতো কাজ করে, যা দিয়ে রিয়েল DOM-এ সংশ্লিষ্ট এলিমেন্ট খুঁজে বের করে প্যাচ প্রয়োগ করা যায়।

প্র ০২ যদি old_tree-এর ul-এর tag ভুল করে "ol" হতো (আর new_tree-এ "ul"-ই থাকত), প্যাচ লিস্ট কেমন দেখাত?

তাহলে কেস ৪ ট্রিগার হতো — root-এই old["tag"] != new["tag"] ("ol" != "ul") সত্যি হয়ে যেত, তাই diff() সাথে সাথে একটিমাত্র প্যাচ রিটার্ন করত: ("replace", (), new_tree) — পুরো new_tree নতুন করে প্রতিস্থাপন করার নির্দেশ, আর children-এর ভেতরের সূক্ষ্ম পরিবর্তনগুলো (টেক্সট বদল, নতুন আইটেম) আলাদাভাবে চিহ্নিত হতো না, কারণ পুরো সাবট্রিই একবারে প্রতিস্থাপিত হয়ে যাচ্ছে।

প্র ০৩ প্রথম আইটেম ("পানি কেনা") এর জন্য কোনো প্যাচ কেন তৈরি হয়নি?

কারণ old_children[0] ও new_children[0] — দুটোই হুবহু একই: একই tag ("li"), একই props ({"class": "todo-item"}), এবং একই টেক্সট চাইল্ড ("পানি কেনা")। কোডের প্রতিটি তুলনা (tag সমান, changed_props খালি ডিকশনারি, টেক্সট সমান) মিথ্যা/খালি ফলাফল দেয়, তাই কোনো প্যাচ যোগ হয় না — এটাই সঠিক আচরণ, কারণ প্রকৃতপক্ষে এই আইটেমটির কিছুই বদলায়নি।

অনুশীলন

  1. চিন্তা করুন: যদি একটি টুডু-লিস্টের মাঝখান থেকে একটি আইটেম বাদ দেওয়া হয় (শেষ থেকে নয়), তাহলে ইনডেক্স-ভিত্তিক এই সরল diff() কী রকম প্যাচ তৈরি করবে — কি সেটা সবচেয়ে দক্ষ (minimal) প্যাচ হবে?

    না, ইনডেক্স-ভিত্তিক তুলনা সবসময় সবচেয়ে দক্ষ প্যাচ দেয় না। মাঝখান থেকে একটি আইটেম বাদ দিলে তার পরের সব আইটেম এক ইনডেক্স করে সরে যায় — ফলে diff() সেগুলোকে "পরিবর্তিত টেক্সট" হিসেবে ধরে নেবে (কারণ ইনডেক্স ১-এর পুরনো টেক্সট আর ইনডেক্স ১-এর নতুন টেক্সট মিলবে না, যদিও আসলে সেটা এক ধাপ সরে যাওয়া একই আইটেম), শুধু শেষে একটি remove_child আসবে। বাস্তব ফ্রেমওয়ার্কগুলো এই সমস্যা সমাধানে প্রতিটি চাইল্জে একটি key প্রপ ব্যবহার করে আইটেমগুলো সঠিকভাবে ম্যাচ করে — এটি একটি পরিচিত ট্রেড-অফ যা এই সরলীকৃত অ্যালগরিদমের বাইরে।

  2. পরীক্ষা করুন: উপরের কোড সেলে old_tree-এর প্রথম li-এর props-এ "data-done": "false" যোগ করুন, আর new_tree-এর একই li-এ "data-done": "true" রাখুন — তারপর Run চেপে দেখুন patch_list-এ নতুন কোন update_props প্যাচ যোগ হয় এবং path কী হয়।

    এই পরিবর্তনের ফলে একটি নতুন প্যাচ যোগ হবে: ("update_props", (0,), {"data-done": "true"}) — path (0,) কারণ এটি root-এর চাইল্ড ইনডেক্স ০ (প্রথম li)। এটাই দেখায় update_props প্যাচ শুধু বদলে যাওয়া প্রপগুলোই ধরে, পুরো props dict নয় — "class": "todo-item" অপরিবর্তিত থাকায় সেটা changed_props-এ আসেনি।

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

  • পরের পাঠ L11 রিরেন্ডার কখন ট্রিগার হয় (state বদলালে) এবং একটি কম্পোনেন্টের সম্পূর্ণ লাইফসাইকেল।
  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট।
  • Python Programming কোর্স সহোদর কোর্স রিকার্সন, ডিকশনারি ও লিস্ট নিয়ে কাজ করার ভাষাগত ভিত্তি সেই কোর্সেই তৈরি হয়েছে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics ও Full-Stack Web Frameworks — সব এক জায়গায়।
আগের পাঠ
কম্পোনেন্ট-ভিত্তিক UI — মূল ধারণা