[{"data":1,"prerenderedAt":732},["ShallowReactive",2],{"blog-\u002Fblog\u002Freact-virtual-dom-reconciliation-guide":3},{"id":4,"title":5,"body":6,"description":717,"difficulty":718,"extension":719,"framework":720,"frameworkSlug":721,"meta":722,"navigation":278,"order":48,"path":723,"qaPath":724,"seo":725,"stem":726,"subtopic":727,"topic":728,"topicSlug":729,"updated":730,"__hash__":731},"blog\u002Fblog\u002Freact-virtual-dom-reconciliation-guide.md","React Virtual DOM and Reconciliation — A Complete Guide",{"type":7,"value":8,"toc":708},"minimark",[9,14,27,34,142,145,149,156,183,340,344,354,361,389,396,400,407,417,514,524,528,531,586,594,598,604,608,614,632,698,704],[10,11,13],"h2",{"id":12},"what-is-the-virtual-dom","What Is the Virtual DOM?",[15,16,17,18,22,23,26],"p",{},"The ",[19,20,21],"strong",{},"virtual DOM (VDOM)"," is a lightweight, in-memory representation of the real DOM. Instead of touching the browser DOM directly on every state change — an expensive operation — React keeps a JavaScript tree of objects describing what the UI should look like. When state changes, React produces a new virtual DOM tree, compares it with the previous one, and only applies the ",[19,24,25],{},"minimal set of real DOM mutations"," needed to bring the two trees into sync.",[15,28,29,30,33],{},"This comparison step is called ",[19,31,32],{},"reconciliation",".",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-jsx shiki shiki-themes github-light github-dark","\u002F\u002F React builds a virtual DOM tree from JSX\nconst element = (\n  \u003Cul>\n    \u003Cli>Alpha\u003C\u002Fli>\n    \u003Cli>Beta\u003C\u002Fli>\n  \u003C\u002Ful>\n);\n\u002F\u002F Internally this becomes a plain JS object:\n\u002F\u002F { type: 'ul', props: { children: [{ type: 'li', ... }, ...] } }\n","jsx","",[42,43,44,53,71,84,100,114,124,130,136],"code",{"__ignoreMap":40},[45,46,49],"span",{"class":47,"line":48},"line",1,[45,50,52],{"class":51},"sJ8bj","\u002F\u002F React builds a virtual DOM tree from JSX\n",[45,54,56,60,64,67],{"class":47,"line":55},2,[45,57,59],{"class":58},"szBVR","const",[45,61,63],{"class":62},"sScJk"," element",[45,65,66],{"class":58}," =",[45,68,70],{"class":69},"sVt8B"," (\n",[45,72,74,77,81],{"class":47,"line":73},3,[45,75,76],{"class":69},"  \u003C",[45,78,80],{"class":79},"s9eBZ","ul",[45,82,83],{"class":69},">\n",[45,85,87,90,93,96,98],{"class":47,"line":86},4,[45,88,89],{"class":69},"    \u003C",[45,91,92],{"class":79},"li",[45,94,95],{"class":69},">Alpha\u003C\u002F",[45,97,92],{"class":79},[45,99,83],{"class":69},[45,101,103,105,107,110,112],{"class":47,"line":102},5,[45,104,89],{"class":69},[45,106,92],{"class":79},[45,108,109],{"class":69},">Beta\u003C\u002F",[45,111,92],{"class":79},[45,113,83],{"class":69},[45,115,117,120,122],{"class":47,"line":116},6,[45,118,119],{"class":69},"  \u003C\u002F",[45,121,80],{"class":79},[45,123,83],{"class":69},[45,125,127],{"class":47,"line":126},7,[45,128,129],{"class":69},");\n",[45,131,133],{"class":47,"line":132},8,[45,134,135],{"class":51},"\u002F\u002F Internally this becomes a plain JS object:\n",[45,137,139],{"class":47,"line":138},9,[45,140,141],{"class":51},"\u002F\u002F { type: 'ul', props: { children: [{ type: 'li', ... }, ...] } }\n",[15,143,144],{},"The payoff: batch updates, predictable re-render semantics, and a layer of indirection that makes server-side rendering and React Native possible without changing application code.",[10,146,148],{"id":147},"the-reconciliation-algorithm-two-core-heuristics","The Reconciliation Algorithm — Two Core Heuristics",[15,150,151,152,155],{},"React's diffing algorithm runs in ",[19,153,154],{},"O(n)"," time (not the theoretical O(n³) for arbitrary trees) by relying on two assumptions:",[157,158,159,173],"ol",{},[92,160,161,164,165,168,169,172],{},[19,162,163],{},"Different element type → tear down and rebuild."," If a ",[42,166,167],{},"\u003Cdiv>"," is replaced by a ",[42,170,171],{},"\u003Cspan>",", React destroys the entire subtree — including component state — and mounts a fresh one. This is why wrapping a component in a container of a different type during a conditional render resets all its children.",[92,174,175,178,179,182],{},[19,176,177],{},"Same element type → update in place."," When the type stays the same, React keeps the existing DOM node and only updates the attributes or props that changed. For class components it calls ",[42,180,181],{},"componentDidUpdate","; for function components it re-invokes the function and reconciles the output.",[35,184,186],{"className":37,"code":185,"language":39,"meta":40,"style":40},"\u002F\u002F Heuristic 1 in action — changing the wrapper type resets state\nfunction Tabs({ isAdmin }) {\n  return isAdmin\n    ? \u003Cdiv>\u003CCounter \u002F>\u003C\u002Fdiv>   \u002F\u002F Counter unmounts when isAdmin flips\n    : \u003Csection>\u003CCounter \u002F>\u003C\u002Fsection>;\n}\n\n\u002F\u002F Fix: keep the type consistent\nfunction Tabs({ isAdmin }) {\n  return \u003Cdiv>{isAdmin ? \u003CAdminPanel \u002F> : \u003CGuestPanel \u002F>}\u003C\u002Fdiv>;\n}\n",[42,187,188,193,211,219,248,269,274,280,285,297,335],{"__ignoreMap":40},[45,189,190],{"class":47,"line":48},[45,191,192],{"class":51},"\u002F\u002F Heuristic 1 in action — changing the wrapper type resets state\n",[45,194,195,198,201,204,208],{"class":47,"line":55},[45,196,197],{"class":58},"function",[45,199,200],{"class":62}," Tabs",[45,202,203],{"class":69},"({ ",[45,205,207],{"class":206},"s4XuR","isAdmin",[45,209,210],{"class":69}," }) {\n",[45,212,213,216],{"class":47,"line":73},[45,214,215],{"class":58},"  return",[45,217,218],{"class":69}," isAdmin\n",[45,220,221,224,227,230,233,237,240,242,245],{"class":47,"line":86},[45,222,223],{"class":58},"    ?",[45,225,226],{"class":69}," \u003C",[45,228,229],{"class":79},"div",[45,231,232],{"class":69},">\u003C",[45,234,236],{"class":235},"sj4cs","Counter",[45,238,239],{"class":69}," \u002F>\u003C\u002F",[45,241,229],{"class":79},[45,243,244],{"class":69},">   ",[45,246,247],{"class":51},"\u002F\u002F Counter unmounts when isAdmin flips\n",[45,249,250,253,255,258,260,262,264,266],{"class":47,"line":102},[45,251,252],{"class":58},"    :",[45,254,226],{"class":69},[45,256,257],{"class":79},"section",[45,259,232],{"class":69},[45,261,236],{"class":235},[45,263,239],{"class":69},[45,265,257],{"class":79},[45,267,268],{"class":69},">;\n",[45,270,271],{"class":47,"line":116},[45,272,273],{"class":69},"}\n",[45,275,276],{"class":47,"line":126},[45,277,279],{"emptyLinePlaceholder":278},true,"\n",[45,281,282],{"class":47,"line":132},[45,283,284],{"class":51},"\u002F\u002F Fix: keep the type consistent\n",[45,286,287,289,291,293,295],{"class":47,"line":138},[45,288,197],{"class":58},[45,290,200],{"class":62},[45,292,203],{"class":69},[45,294,207],{"class":206},[45,296,210],{"class":69},[45,298,300,302,304,306,309,312,314,317,320,323,325,328,331,333],{"class":47,"line":299},10,[45,301,215],{"class":58},[45,303,226],{"class":69},[45,305,229],{"class":79},[45,307,308],{"class":69},">{isAdmin ",[45,310,311],{"class":58},"?",[45,313,226],{"class":69},[45,315,316],{"class":235},"AdminPanel",[45,318,319],{"class":69}," \u002F> ",[45,321,322],{"class":58},":",[45,324,226],{"class":69},[45,326,327],{"class":235},"GuestPanel",[45,329,330],{"class":69}," \u002F>}\u003C\u002F",[45,332,229],{"class":79},[45,334,268],{"class":69},[45,336,338],{"class":47,"line":337},11,[45,339,273],{"class":69},[10,341,343],{"id":342},"fiber-reacts-reconciliation-engine","Fiber — React's Reconciliation Engine",[15,345,346,349,350,353],{},[19,347,348],{},"React Fiber"," (introduced in React 16) is a complete rewrite of the reconciliation engine. The key idea: break rendering work into small units called ",[19,351,352],{},"fibers"," (one per component) that can be paused, aborted, or restarted.",[15,355,356,357,360],{},"Before Fiber, reconciliation was a synchronous recursive walk. A large tree could block the main thread for tens of milliseconds — causing dropped frames. Fiber makes the work ",[19,358,359],{},"interruptible",", which unlocks:",[80,362,363,377,383],{},[92,364,365,368,369,372,373,376],{},[19,366,367],{},"Concurrent rendering"," — React 18's ",[42,370,371],{},"startTransition",", ",[42,374,375],{},"useDeferredValue",", and Suspense all depend on Fiber's ability to pause and resume.",[92,378,379,382],{},[19,380,381],{},"Priority scheduling"," — urgent updates (user input) can preempt lower-priority work (a background data fetch re-render).",[92,384,385,388],{},[19,386,387],{},"Error boundaries"," — Fiber tracks which subtree threw, so React can unmount only that subtree and display a fallback.",[15,390,391,392,395],{},"The reconciliation pass produces a ",[19,393,394],{},"work-in-progress tree",". React only commits that tree to the real DOM after the entire diff is complete, keeping the UI consistent at all times.",[10,397,399],{"id":398},"keys-the-third-heuristic","Keys — The Third Heuristic",[15,401,402,403,406],{},"When reconciling ",[19,404,405],{},"lists",", React needs to match old children to new ones. Without hints it falls back to index-based matching, which causes subtle bugs when items are reordered or inserted.",[15,408,17,409,412,413,416],{},[42,410,411],{},"key"," prop tells React the ",[19,414,415],{},"stable identity"," of each list item:",[35,418,420],{"className":37,"code":419,"language":39,"meta":40,"style":40},"\u002F\u002F Bad — index-based matching breaks on reorder\u002Finsert\n{items.map((item, i) => \u003CRow key={i} data={item} \u002F>)}\n\n\u002F\u002F Good — stable identity\n{items.map(item => \u003CRow key={item.id} data={item} \u002F>)}\n",[42,421,422,427,474,478,483],{"__ignoreMap":40},[45,423,424],{"class":47,"line":48},[45,425,426],{"class":51},"\u002F\u002F Bad — index-based matching breaks on reorder\u002Finsert\n",[45,428,429,432,435,438,441,443,446,449,452,454,457,460,463,466,469,471],{"class":47,"line":55},[45,430,431],{"class":69},"{items.",[45,433,434],{"class":62},"map",[45,436,437],{"class":69},"((",[45,439,440],{"class":206},"item",[45,442,372],{"class":69},[45,444,445],{"class":206},"i",[45,447,448],{"class":69},") ",[45,450,451],{"class":58},"=>",[45,453,226],{"class":69},[45,455,456],{"class":235},"Row",[45,458,459],{"class":62}," key",[45,461,462],{"class":58},"=",[45,464,465],{"class":69},"{i} ",[45,467,468],{"class":62},"data",[45,470,462],{"class":58},[45,472,473],{"class":69},"{item} \u002F>)}\n",[45,475,476],{"class":47,"line":73},[45,477,279],{"emptyLinePlaceholder":278},[45,479,480],{"class":47,"line":86},[45,481,482],{"class":51},"\u002F\u002F Good — stable identity\n",[45,484,485,487,489,492,494,497,499,501,503,505,508,510,512],{"class":47,"line":102},[45,486,431],{"class":69},[45,488,434],{"class":62},[45,490,491],{"class":69},"(",[45,493,440],{"class":206},[45,495,496],{"class":58}," =>",[45,498,226],{"class":69},[45,500,456],{"class":235},[45,502,459],{"class":62},[45,504,462],{"class":58},[45,506,507],{"class":69},"{item.id} ",[45,509,468],{"class":62},[45,511,462],{"class":58},[45,513,473],{"class":69},[15,515,516,517,520,521,523],{},"A key change is equivalent to changing the element type: React unmounts the old component (losing its state) and mounts a fresh one. This is occasionally useful as an ",[19,518,519],{},"intentional reset trick"," — passing a new ",[42,522,411],{}," to force a child to reinitialise.",[10,525,527],{"id":526},"render-phase-vs-commit-phase","Render Phase vs Commit Phase",[15,529,530],{},"Reconciliation has two distinct phases:",[532,533,534,550],"table",{},[535,536,537],"thead",{},[538,539,540,544,547],"tr",{},[541,542,543],"th",{},"Phase",[541,545,546],{},"What happens",[541,548,549],{},"Can be interrupted?",[551,552,553,567],"tbody",{},[538,554,555,561,564],{},[556,557,558],"td",{},[19,559,560],{},"Render",[556,562,563],{},"React calls component functions, diffs the output",[556,565,566],{},"Yes (Concurrent Mode)",[538,568,569,574,583],{},[556,570,571],{},[19,572,573],{},"Commit",[556,575,576,577,372,580],{},"DOM mutations, ",[42,578,579],{},"useLayoutEffect",[42,581,582],{},"useEffect",[556,584,585],{},"No — runs synchronously to completion",[15,587,588,589,372,591,593],{},"Side effects must live in the commit phase (",[42,590,582],{},[42,592,579],{},"). Running them during the render phase would break concurrent rendering because that phase can be replayed.",[10,595,597],{"id":596},"hydration","Hydration",[15,599,600,601,603],{},"In server-side or statically rendered React, the browser receives pre-built HTML. ",[19,602,597],{}," is the process of attaching event handlers and initialising component state on top of that existing markup — without discarding and rebuilding the DOM. React walks the server-rendered tree and the client VDOM tree in parallel; a mismatch (hydration error) causes React to fall back to a full client-side re-render for the affected subtree, which is expensive and visible to users.",[10,605,607],{"id":606},"finding-bottlenecks-with-react-devtools-profiler","Finding Bottlenecks with React DevTools Profiler",[15,609,17,610,613],{},[19,611,612],{},"React DevTools Profiler"," records which components rendered, how long each took, and why they rendered (prop change, state change, context change, hooks). Key workflow:",[157,615,616,619,622,625],{},[92,617,618],{},"Open DevTools → Profiler tab → click Record.",[92,620,621],{},"Interact with the slow part of the UI, then stop recording.",[92,623,624],{},"Inspect the flame graph for unexpectedly wide bars — components that re-rendered but should have bailed out.",[92,626,627,628,631],{},"Check the \"Why did this render?\" tooltip. If it says \"parent re-rendered\" for a pure display component, that's a ",[42,629,630],{},"React.memo"," candidate.",[35,633,635],{"className":37,"code":634,"language":39,"meta":40,"style":40},"\u002F\u002F Enable why-did-you-render in development for automated warnings\nimport whyDidYouRender from '@welldone-software\u002Fwhy-did-you-render';\nif (process.env.NODE_ENV === 'development') {\n  whyDidYouRender(React, { trackAllPureComponents: true });\n}\n",[42,636,637,642,660,680,694],{"__ignoreMap":40},[45,638,639],{"class":47,"line":48},[45,640,641],{"class":51},"\u002F\u002F Enable why-did-you-render in development for automated warnings\n",[45,643,644,647,650,653,657],{"class":47,"line":55},[45,645,646],{"class":58},"import",[45,648,649],{"class":69}," whyDidYouRender ",[45,651,652],{"class":58},"from",[45,654,656],{"class":655},"sZZnC"," '@welldone-software\u002Fwhy-did-you-render'",[45,658,659],{"class":69},";\n",[45,661,662,665,668,671,674,677],{"class":47,"line":73},[45,663,664],{"class":58},"if",[45,666,667],{"class":69}," (process.env.",[45,669,670],{"class":235},"NODE_ENV",[45,672,673],{"class":58}," ===",[45,675,676],{"class":655}," 'development'",[45,678,679],{"class":69},") {\n",[45,681,682,685,688,691],{"class":47,"line":86},[45,683,684],{"class":62},"  whyDidYouRender",[45,686,687],{"class":69},"(React, { trackAllPureComponents: ",[45,689,690],{"class":235},"true",[45,692,693],{"class":69}," });\n",[45,695,696],{"class":47,"line":102},[45,697,273],{"class":69},[15,699,700,703],{},[19,701,702],{},"Rule of thumb:"," optimise reconciliation at the data boundary — keep state as low as possible in the tree and memoize only after the Profiler confirms a problem. Premature memoization adds overhead without benefit.",[705,706,707],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":40,"searchDepth":55,"depth":55,"links":709},[710,711,712,713,714,715,716],{"id":12,"depth":55,"text":13},{"id":147,"depth":55,"text":148},{"id":342,"depth":55,"text":343},{"id":398,"depth":55,"text":399},{"id":526,"depth":55,"text":527},{"id":596,"depth":55,"text":597},{"id":606,"depth":55,"text":607},"React virtual DOM and reconciliation interview questions — diffing algorithm, Fiber architecture, keys, bailout conditions, and how React decides what to re-render.","medium","md","React","react",{},"\u002Fblog\u002Freact-virtual-dom-reconciliation-guide","\u002Freact\u002Frendering-and-performance\u002Fvirtual-dom-reconciliation",{"title":5,"description":717},"blog\u002Freact-virtual-dom-reconciliation-guide","Virtual DOM and Reconciliation","Rendering and Performance","rendering-and-performance","2026-06-24","LFQhKDHIav57Ab0gcMlkneDGcOc2CzXlyCnSmHpLaJg",1782244083220]