From 9c364d8498c9a46b0237cd7b61cf3a5c361f1d9d Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Tue, 23 Jun 2026 02:54:28 -0400 Subject: [PATCH] perf(#4346): recycle stable DOM nodes during virtual-scroll measurement re-renders # Conflicts: # static/ui.js # tests/test_issue4346_vscroll_footer_jitter.py --- static/ui.js | 56 +++++++++++++++---- tests/test_issue4346_vscroll_footer_jitter.py | 52 +++++++++++++++++ 2 files changed, 97 insertions(+), 11 deletions(-) diff --git a/static/ui.js b/static/ui.js index 1c9f759c..02c1544c 100644 --- a/static/ui.js +++ b/static/ui.js @@ -436,6 +436,8 @@ let _messageVirtualMeasurementRetryCount=0; let _messageVirtualScrollActive=false; let _messageVirtualScrollSettleTimer=0; let _messageVirtualDeferredMeasurement=null; +let _msgNodeRecycleEnabled=false; +const _recycleStash=new Map(); function _markMessageVirtualScrollActive(){ _messageVirtualScrollActive=true; clearTimeout(_messageVirtualScrollSettleTimer); @@ -965,7 +967,9 @@ function _scheduleMessageVirtualizedRender(force){ const liveWindow=_currentMessageVirtualWindow(liveVisWithIdx,_messageVirtualKeepTailCount()); const liveKey=_messageVirtualWindowKeyFor(liveWindow); if(!force&&liveKey===_messageVirtualWindowKey) return; - _compensateScrollForMeasurementDelta(()=>{ renderMessages({ preserveScroll:true }); }); + _msgNodeRecycleEnabled=true; + try{ _compensateScrollForMeasurementDelta(()=>{ renderMessages({ preserveScroll:true }); }); } + finally{ _msgNodeRecycleEnabled=false; } }); } @@ -11114,6 +11118,14 @@ function renderMessages(options){ S.session && typeof S.session.compression_anchor_summary==='string' ) ? S.session.compression_anchor_summary.trim() : ''; const worklogDetailDisclosureState=_captureWorklogDetailDisclosureState(inner); + _recycleStash.clear(); + if(_msgNodeRecycleEnabled){ + for(const child of Array.from(inner.children)){ + if(!child.dataset||!child.dataset.msgIdx) continue; + if(child.id==='liveAssistantTurn'||child.querySelector&&child.querySelector('#liveAssistantTurn')) continue; + _recycleStash.set(Number(child.dataset.msgIdx), child); + } + } inner.innerHTML=''; const compressionNode=compressionState?_compressionCardsNode(compressionState):null; const {message:referenceMessage, rawIdx:referenceMessageRawIdx}=_latestCompressionReferenceMessage( @@ -11381,22 +11393,43 @@ function renderMessages(options){ if(isUser){ currentAssistantTurn=null; - const row=document.createElement('div'); - row.className='msg-row'; - row.id=_userMessageDomId(rawIdx); - row.dataset.msgIdx=rawIdx; - row.dataset.sessionMsgIdx=_messageSessionIndexForRawIdx(rawIdx); - row.dataset.messageAnchorKey=_messageViewportAnchorKeyForMessage(m); - row.dataset.role='user'; - row.dataset.rawText=String(displayContent).trim(); - row.innerHTML=`${filesHtml}
${bodyHtml}
${footHtml}`; + let row=_msgNodeRecycleEnabled?_recycleStash.get(rawIdx):null; + const newRawText=String(displayContent).trim(); + if(row){ + row.id=_userMessageDomId(rawIdx); + row.dataset.msgIdx=rawIdx; + row.dataset.sessionMsgIdx=_messageSessionIndexForRawIdx(rawIdx); + row.dataset.messageAnchorKey=_messageViewportAnchorKeyForMessage(m); + row.dataset.role='user'; + if(row.dataset.rawText!==newRawText){ + row.dataset.rawText=newRawText; + row.innerHTML=`${filesHtml}
${bodyHtml}
${footHtml}`; + } + }else{ + row=document.createElement('div'); + row.className='msg-row'; + row.id=_userMessageDomId(rawIdx); + row.dataset.msgIdx=rawIdx; + row.dataset.sessionMsgIdx=_messageSessionIndexForRawIdx(rawIdx); + row.dataset.messageAnchorKey=_messageViewportAnchorKeyForMessage(m); + row.dataset.role='user'; + row.dataset.rawText=newRawText; + row.innerHTML=`${filesHtml}
${bodyHtml}
${footHtml}`; + } inner.appendChild(row); userRows.set(rawIdx, row); continue; } if(!currentAssistantTurn){ - currentAssistantTurn=_createAssistantTurn(tsTitle, isTpsDisplayEnabled()?_formatTurnTps(m._turnTps):''); + const recycled=_msgNodeRecycleEnabled?_recycleStash.get(rawIdx):null; + if(recycled){ + const blocks=_assistantTurnBlocks(recycled); + if(blocks) blocks.innerHTML=''; + currentAssistantTurn=recycled; + }else{ + currentAssistantTurn=_createAssistantTurn(tsTitle, isTpsDisplayEnabled()?_formatTurnTps(m._turnTps):''); + } inner.appendChild(currentAssistantTurn); } const seg=document.createElement('div'); @@ -12273,6 +12306,7 @@ function renderMessages(options){ } } _updateMessageVirtualMeasurements(renderVisWithIdx, renderVisibleIdxs, virtualWindow); + _recycleStash.clear(); } function _toolDisplayName(tc){ diff --git a/tests/test_issue4346_vscroll_footer_jitter.py b/tests/test_issue4346_vscroll_footer_jitter.py index d7afca96..7fce04ae 100644 --- a/tests/test_issue4346_vscroll_footer_jitter.py +++ b/tests/test_issue4346_vscroll_footer_jitter.py @@ -51,3 +51,55 @@ def test_js_try_finally_guards_class_removal(): assert remove_idx != -1, "missing classList.remove('vscroll-measuring')" assert try_idx < finally_idx < remove_idx, \ "classList.remove must remain in the finally{} cleanup path" + + +def test_js_recycle_flag_exists(): + """ui.js declares the _msgNodeRecycleEnabled flag.""" + assert '_msgNodeRecycleEnabled' in JS + + +def test_js_recycle_stash_exists(): + """ui.js declares the _recycleStash Map.""" + assert '_recycleStash' in JS + + +def test_js_recycle_flag_set_in_virtual_render(): + """_scheduleMessageVirtualizedRender sets _msgNodeRecycleEnabled=true + before the compensate call and clears it in finally.""" + fn_match = re.search( + r'function _scheduleMessageVirtualizedRender\(force\)\{(.+?)^(?=function )', + JS, re.DOTALL | re.MULTILINE + ) + assert fn_match, "_scheduleMessageVirtualizedRender not found" + body = fn_match.group(1) + assert '_msgNodeRecycleEnabled=true' in body + finally_match = re.search(r'finally\{([^}]*)\}', body) + assert finally_match, "no finally block in _scheduleMessageVirtualizedRender" + assert '_msgNodeRecycleEnabled=false' in finally_match.group(1) + + +def test_js_stash_populated_before_wipe(): + """The recycleStash population loop appears before innerHTML='' in renderMessages.""" + fn_match = re.search( + r'function renderMessages\(options\)\{(.+?)^(?=function )', + JS, re.DOTALL | re.MULTILINE + ) + assert fn_match, "renderMessages not found" + body = fn_match.group(1) + stash_pos = body.find('_recycleStash.set(') + wipe_pos = body.find("inner.innerHTML='';") + assert stash_pos != -1, "_recycleStash.set not found in renderMessages" + assert wipe_pos != -1, "innerHTML wipe not found in renderMessages" + assert stash_pos < wipe_pos, "_recycleStash.set must appear before innerHTML=''" + + +def test_js_user_row_checks_stash(): + """The user-row creation block checks _recycleStash before createElement.""" + fn_match = re.search( + r'function renderMessages\(options\)\{(.+?)^(?=function )', + JS, re.DOTALL | re.MULTILINE + ) + assert fn_match, "renderMessages not found" + body = fn_match.group(1) + assert '_recycleStash.get(rawIdx)' in body, \ + "user row block must check _recycleStash.get(rawIdx)"