v0.4.2 : refresh instantane du chat (fin du layout thrashing O(n2) au replay)

Au refresh, le client rejouait tout le journal depuis seq 0 et forcait un reflow
synchrone par evenement (scrollMaybe lisait scrollHeight ; collapseInstant lisait
offsetHeight) sur un DOM grandissant -> cout quadratique, 20-30s pour que les
derniers messages s'affichent. Le scroll est desormais fait une seule fois a la
fin du replay (caught_up), plus aucun reflow force pendant le rejeu -> O(n).
This commit is contained in:
nathaninline committed 2026-07-22 08:55:07 +02:00
1 parent e16bebda8a
commit 7d104e3213
5 files changed
+18 -7

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ import (
"strings"
)
const Version = "0.4.1"
const Version = "0.4.2"
func main() {
// Migration one-shot des anciens skills (SKILLS/<nom>/SKILL.md) vers la
Binary file not shown.
Binary file not shown.
+14 -3
View File
@@ -1188,6 +1188,11 @@ function isNearBottom(){
return c.scrollHeight - c.scrollTop - c.clientHeight < 60;
}
function scrollMaybe(){
// Pendant le replay initial on NE force AUCUN reflow : lire scrollHeight à chaque
// événement rejoué = un layout synchrone forcé sur un DOM qui grossit → coût
// quadratique (20-30 s de rendu au refresh sur un long fil). Le scroll est fait
// une seule fois à la fin du replay, via jumpBottom() au signal {caught_up}.
if(REPLAYING) return;
if(stickyBottom){
const c = chatEl();
c.scrollTop = c.scrollHeight;
@@ -1261,9 +1266,12 @@ function collapseAll(list){ for(const el of list){ if(el) collapseBody(el); } li
function collapseInstant(el){
const bw=el.querySelector('.bodywrap'); if(!bw) return;
el.classList.add('collapsed');
const t=bw.style.transition; bw.style.transition='none';
// Pas de `void bw.offsetHeight` ici : la bulle vient d'être créée et n'a jamais
// été peinte dépliée, donc poser height:0 n'anime pas — inutile de forcer un
// reflow par bulle (ce qui, multiplié par le replay, coûtait très cher).
bw.style.transition='none';
bw.style.height='0px'; bw.style.width='0px';
void bw.offsetHeight; bw.style.transition=t;
requestAnimationFrame(()=>{ bw.style.transition=''; });
}
function setLabel(el, text){ el.querySelector('.label').textContent = text; }
function bodyOf(el){ return el.querySelector('.body'); }
@@ -1391,7 +1399,10 @@ function restoreChat(){
// sauter en bas (effet de clignotement). Il est révélé, positionné en bas, au
// signal {caught_up}. Filet de sécurité : révélé quoi qu'il arrive après 2s.
const c=chatEl(); c.style.opacity='0';
setTimeout(()=>{ c.style.transition='opacity .15s'; c.style.opacity='1'; }, 2000);
// Si {caught_up} tarde au-delà de 2s (replay anormalement long), on révèle quand
// même — et on saute en bas DIRECTEMENT (scrollMaybe est neutralisé tant que
// REPLAYING, donc on force ici le positionnement).
setTimeout(()=>{ c.style.transition='opacity .15s'; c.style.opacity='1'; c.scrollTop=c.scrollHeight; }, 2000);
connectStream();
}
function onKey(e){ if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); send(); } }
+3 -3
View File
@@ -3,13 +3,13 @@
"FileVersion": {
"Major": 0,
"Minor": 4,
"Patch": 1,
"Patch": 2,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 4,
"Patch": 1,
"Patch": 2,
"Build": 0
},
"FileFlagsMask": "3f",
@@ -25,7 +25,7 @@
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
"OriginalFilename": "jean.exe",
"ProductName": "Jean",
"ProductVersion": "0.4.1",
"ProductVersion": "0.4.2",
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
},
"VarFileInfo": {