From 7d104e3213a0cba84b93e2221c8c1985005e2794 Mon Sep 17 00:00:00 2001 From: nathaninline Date: Wed, 22 Jul 2026 08:55:07 +0200 Subject: [PATCH] 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). --- main.go | 2 +- resource_windows_amd64.syso | Bin 8340 -> 8340 bytes resource_windows_arm64.syso | Bin 8340 -> 8340 bytes ui/index.html | 17 ++++++++++++++--- versioninfo.json | 6 +++--- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 48d671e..8b276a5 100644 --- a/main.go +++ b/main.go @@ -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//SKILL.md) vers la diff --git a/resource_windows_amd64.syso b/resource_windows_amd64.syso index ddea813998b0256c3d6620f8f1486c124952bfea..ba9f97a2903bd26d8ab431dd1f0bccec8832a847 100644 GIT binary patch delta 36 ncmbQ@IK^>;lnf^m0}BHPFl|;lnf^$0}BHPFm6_s5oBUC+-xcr%m}19<+&LFgG~m? diff --git a/resource_windows_arm64.syso b/resource_windows_arm64.syso index 45bdb60cb614539db33ce1bb2791fee8d1875933..7ae2f6834f875722055f42a2336074725849de3d 100644 GIT binary patch delta 36 ncmbQ@IK^>;lnf^m0}BHPFl|;lnf^$0}BHPFm6_s5oBUC+-xcr%m}19<+&LFgG~m? diff --git a/ui/index.html b/ui/index.html index be37410..fbfcdbe 100644 --- a/ui/index.html +++ b/ui/index.html @@ -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(); } } diff --git a/versioninfo.json b/versioninfo.json index e846aaa..93211ed 100644 --- a/versioninfo.json +++ b/versioninfo.json @@ -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": {