A chatablak tetejére megérkezik húsz régi üzenet, egy hirdetés betölti a helyét, vagy egy kép végre megkapja a valódi magasságát. Ha az olvasó épp egy régebbi bejegyzést néz, nem a dokumentum abszolút scrollpozícióját akarja megtartani, hanem azt a sort, ahol tartott. Ezt próbálja alapértelmezetten megoldani a böngésző scroll anchoring mechanizmusa.
A funkció legtöbbször láthatatlanul hasznos. De amikor egy saját végtelen listához kézzel igazítjuk a scrollTop-ot, a böngésző és az alkalmazáskód ugyanazért a pixelekért versenyez. Ilyenkor jelennek meg a „néha kétszer ugrik” jellegű hibák.

Nem egy rögzített ponthoz, hanem egy elemhez köt
A CSS Scroll Anchoring specifikáció szerint a böngésző egy látható DOM-elemet választ horgonynak. Ha egy felette lévő változás eltolja ezt az elemet, a scrollert ellenirányban elmozdítja. Az eredmény: a horgony nagyjából ugyanott marad a viewportban. A választás nem API-szerződés: mély, látható elemek előnyt élveznek, a fókuszban lévő elem és a keresési találat pedig kiemelt jelölt lehet.
Ezért nincs értelme arra építeni, hogy „a harmadik kártya biztosan horgony”. A mechanizmust layoutváltozások csillapítására kapjuk, nem pozicionáló API-ként. A kompenzáció ráadásul bizonyos változásoknál elnyomható; a specifikáció ilyenek között említi többek közt a transform, a méretek, a margó vagy a pozicionálás módosítását az érintett útvonalon.
A két jó stratégia
Hírfolyamnál, cikkoldalnál vagy képeket utólag betöltő felületnél általában hagyjuk dolgozni a böngészőt. Emellett továbbra is érdemes helyet foglalni a később érkező tartalomnak: a horgonyzás a már elgörgetett részt védi, nem helyettesíti a stabil layoutot és a képek méretmegadását.
Fordított chat- vagy naplólistánál viszont gyakran saját szabály kell: ha a felhasználó az alján van, maradjon az alján; ha régi üzeneteket olvas, az első látható üzenet maradjon a helyén. Ebben az esetben egyetlen gazdája legyen a scrollpozíciónak. Kapcsoljuk ki az automatikát azon a scrolleren a overflow-anchor: none szabállyal, majd a beszúrás előtti és utáni tartalommagasság különbségével korrigáljunk.
function nextScrollTop({ scrollTop, oldScrollHeight, newScrollHeight, clientHeight }) {
const atBottom = oldScrollHeight - scrollTop - clientHeight <= 1;
const addedHeight = newScrollHeight - oldScrollHeight;
return atBottom
? newScrollHeight - clientHeight
: scrollTop + addedHeight;
}
A tényleges beszúrás előtt olvassuk ki a scrollHeight, szúrjuk be a régi elemeket a lista elejére, majd az új scrollHeight és a függvény alapján állítsuk a scrollTop-ot. A scrollHeight a túlcsorduló tartalmat is méri, ezért pont azt a magasságtöbbletet adja, amelyet a felhasználó előtt kompenzálni kell. Az egy pixeles tolerancia a kerekítési eltérések miatt praktikus.
Virtualizált listánál ez csak akkor működik, ha a mérés és a kompenzáció ugyanabban a renderciklusban látja a ténylegesen megváltozott magasságot. Ha a sorok magasságát később egy kép, betűkészlet vagy aszinkron mérés növeli meg, azt külön kezelnünk kell, vagy hagynunk kell a natív horgonyzást. A két stratégia közötti választás tehát nem teljesítménytrükk, hanem tulajdonjog: a böngésző vagy az alkalmazás kezelje a stabil olvasási pozíciót.
Érdemes a saját kompenzációt a lista belső scrollerére korlátozni. Ne tegyünk automatikus scrollIntoView()-hívást minden új üzenethez: azzal az olvasó régi üzenetek között elfoglalt helyét írjuk felül. Az alsó pozíciót csak akkor kövessük, ha a beszúrás előtt valóban ott volt.
Ne keverjük ezt a scrollIntoView()-val. Az utóbbi aktív navigációs utasítás: egy konkrét elemet a látható tartományba visz. Scroll anchoringnál épp az a cél, hogy DOM-változás közben ne kelljen új helyre navigálnunk.
Az egyirányú kikapcsolás csapdája
Az overflow-anchor: none nem öröklődik, mégis kizárja az adott elem teljes részfáját a horgonyjelöltek közül. Ezt egy gyermek overflow-anchor: auto szabállyal nem lehet egyszerűen visszakapcsolni; kivétel a beágyazott, önálló scroller. Emiatt ne a body-ra tegyük hibakeresésből: egy közeli chatlistára vagy virtualizált panelre szűkítsük.
A jó gyors ellenőrzés: lassítsuk le a hálózatot, görgessünk a lista közepére, majd kérjünk régebbi oldalnyi adatot. Ha a tartalom mégis ugrik, előbb azt derítsük ki, ki írja a scrollTop-ot. Az esetek nagy részében nem a képlet rossz, hanem két, egymástól független kompenzáció fut egyszerre.