diag(mobile): instrument slide frame-timing + render cost + load-older scroll to find the real jank
Re-add /api/dbg + a frame-gap/long-task probe over the chat-open slide, the innerHTML render time, and before/after scroll for load-older — to measure on-device where the jank actually is (main-thread block vs compositor) instead of guessing. Temporary; removed once fixed. Build marker -> 2026-07-19-batch122. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+22
-2
@@ -1140,7 +1140,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<script src="/icons.js?v=6"></script>
|
||||
<script>window.__BUILD='2026-07-18-batch121';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
<script>window.__BUILD='2026-07-19-batch122';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
// Emoji are rendered with the OS's own (colour) emoji font — instant, zero network.
|
||||
//
|
||||
// We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from
|
||||
@@ -1150,6 +1150,22 @@
|
||||
// existing call sites don't have to change.
|
||||
function twemojify(_el){ /* native emoji: nothing to do */ }</script>
|
||||
<script>
|
||||
// TEMP perf diagnostics → /api/dbg (server log). Remove once slide/scroll jank is fixed.
|
||||
window.bzDbg=function(tag,extra){ try{ var p={tag:tag,build:window.__BUILD}; if(extra) for(var k in extra) p[k]=extra[k]; fetch('/api/dbg',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)}).catch(function(){}); }catch(e){} };
|
||||
// Measures frame gaps + long tasks over a window (the chat-open slide). Big maxGap / long tasks == the
|
||||
// main thread is blocked (jank); small gaps == the compositor is running the slide smoothly.
|
||||
window.__perfProbe=function(label){ try{
|
||||
var frames=[], last=performance.now(), start=last, longtasks=[], po=null;
|
||||
try{ po=new PerformanceObserver(function(l){ l.getEntries().forEach(function(e){ longtasks.push(Math.round(e.duration)); }); }); po.observe({entryTypes:['longtask']}); }catch(_){}
|
||||
(function tick(now){ frames.push(Math.round(now-last)); last=now;
|
||||
if(now-start<650){ requestAnimationFrame(tick); }
|
||||
else { if(po) try{po.disconnect();}catch(_){}
|
||||
var max=0; for(var i=1;i<frames.length;i++) if(frames[i]>max) max=frames[i];
|
||||
var over=frames.filter(function(d){return d>24;}).length;
|
||||
window.bzDbg('slidePerf',{label:label||'', frames:frames.length, maxGapMs:max, jankFrames:over, longtasks:longtasks, gaps:frames.slice(0,40)}); }
|
||||
})(last);
|
||||
}catch(e){} };
|
||||
<script>
|
||||
// Native shell tag (iOS/Android). Safe-area insets are handled natively by the @capacitor-community/
|
||||
// safe-area plugin (it makes env(safe-area-inset-*) report real values), so there's no JS status-bar
|
||||
// juggling here — we only tag <html> so CSS can make small native-only tweaks if needed.
|
||||
@@ -1498,8 +1514,11 @@ async function loadOlder(){
|
||||
const have=new Set(THREAD.map(m=>m.id)); const add=older.filter(m=>!have.has(m.id));
|
||||
if(add.length){ THREAD=add.concat(THREAD); THREAD_CACHE.set(kind+':'+id, THREAD.slice()); }
|
||||
if(older.length<PAGE) _hasMoreOlder=false;
|
||||
if(window.__perfProbe) window.__perfProbe('older'); // TEMP: measure frame timing during the older-page render
|
||||
const _beforeTop = box?box.scrollTop:0;
|
||||
renderThread(true); // keep scroll (don't jump to bottom)
|
||||
_restore(); // put the anchor message back at its exact viewport position
|
||||
if(window.bzDbg && box) window.bzDbg('older',{added:add.length, beforeScrollTop:Math.round(_beforeTop), afterScrollTop:Math.round(box.scrollTop), scrollH:Math.round(box.scrollHeight)}); // TEMP: did the anchor hold?
|
||||
if(box){ const imgs=[...box.querySelectorAll('.bubble img')].filter(im=>!im.complete); imgs.forEach(im=>im.addEventListener('load', _restore, {once:true})); setTimeout(()=>imgs.forEach(im=>{ try{ im.removeEventListener('load', _restore); }catch(_){} }), 3000); } // re-anchor as prepended images load (they'd shift it down)
|
||||
} else { _hasMoreOlder=false; }
|
||||
_loadingOlder=false; _olderCd=Date.now()+500; // brief cooldown so it can't re-fire and fight the scroll
|
||||
@@ -2700,7 +2719,7 @@ function renderThread(keepScroll){
|
||||
if(!THREAD.length){ box.innerHTML='<div class="empty-thread">No messages yet — say hello 👋</div>'; return; }
|
||||
let html='';
|
||||
for(const m of THREAD){ const dk=dayKey(m.created_at); if(dk!==_lastDay){ html+='<div class="day-sep"><span>'+pEsc(dayLabel(m.created_at))+'</span></div>'; _lastDay=dk; } rendered.add(m.id); html+=bubbleHTML(m); }
|
||||
box.innerHTML=html; twemojify(box);
|
||||
var _rt0=performance.now(); box.innerHTML=html; twemojify(box); if(window.bzDbg && !keepScroll) window.bzDbg('render',{n:THREAD.length, innerHtmlKb:Math.round(html.length/1024), renderMs:Math.round(performance.now()-_rt0)}); // TEMP: how long the sync DOM build costs
|
||||
if(!keepScroll){
|
||||
if(_convoRevealed){
|
||||
// #7: a later render during the SAME open (network arriving after the cache render) — just stay at
|
||||
@@ -2764,6 +2783,7 @@ function armOpenSlide(){
|
||||
if(c) c.addEventListener('transitionend', _slideEndHandler);
|
||||
requestAnimationFrame(()=>requestAnimationFrame(()=>{
|
||||
if(window.__slideTok!==token) return;
|
||||
if(window.__perfProbe) window.__perfProbe('slide'); // TEMP: measure frame timing over the slide
|
||||
b.classList.add('chat-anim'); // compositor now runs transform 100%->0
|
||||
window.__chatOpenT=setTimeout(done, 360); // fallback if transitionend is missed (300ms + margin)
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user