滾動錨定(Scroll Anchoring)- 讓視口內容不再因視口上方 DOM 元素的高度變化而產生跳動

紫雲飛發表於2017-04-05

不知道你有沒有經歷過這樣的場景:當你開啟一張“多圖殺貓”的頁面後,正一張圖一張圖邊滾邊看,在你剛準備定睛看某一張圖的時候,這張圖突然被它上面的內容擠到了視口下方,然後你趕緊把滾動條往下拉,試圖追趕這張沒看完的圖,當你剛剛追上的時候,這張圖又一次被擠到了你看不見的地方。

發生這種情況的原因是因為在很多場景下(比如論壇裡),你沒法事先知道一張圖的高度,所以你沒法事先給這張圖佔位,在網速不理想的情況下,可能就會發生我上面描述的這種因頁面靠上的圖片比靠下的圖片晚載入出來而導致使用者當前瀏覽的內容被頻繁擠出視口的情況。

我通過在定時器回撥裡向頁面上方插入圖片來模擬一下剛才描述的這種情況:

<style>
  img {
    display: block;
    margin: 0 auto;
  }
</style>
<img src="https://aecpm.alicdn.com/tfscom/TB1.52aPFXXXXa0XXXXXXXXXXXX.jpg">
<img src="https://aecpm.alicdn.com/tfscom/TB1_utRPVXXXXapXVXXXXXXXXXX.png">
<img src="https://static.dingtalk.com/media/lAHOuOFd_czSzQEn_295_210.gif">
<img src="https://aecpm.alicdn.com/tfscom/TB1f1xwQpXXXXXBXVXXXXXXXXXX.jpg">
<img src="https://gtms03.alicdn.com/tps/i3/TB1eSxvJVXXXXaKXFXXYoAvIXXX-220-50.png">
<img src="https://gw.alicdn.com/bao/uploaded/TB1EGvvPVXXXXX3aXXXXXXXXXXX-200-200.jpg">
<img src="https://gw.alicdn.com/tfscom/TB1CLTHNFXXXXaDXpXXXXXXXXXX">
<script>
  const urls = `
https://asearch.alicdn.com/bao/uploaded/i1/1381306006414474986/TB2_gZAlNtmpuFjSZFqXXbHFpXa_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i1/153360285303496277/TB2SO.Wa4vzQeBjSZFEXXbYEpXa_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i1/188050339412916381/TB2geTXaypnpuFjSZFkXXc4ZpXa_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i2/181720289489216985/TB2UFz6amjz11Bjy0FnXXcnxXXa_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i3/108480250457898935/TB28r5osFXXXXbrXXXXXXXXXXXX_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i3/111180208599309441/TB2kAsQnVXXXXXcXFXXXXXXXXXX_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i3/171530328819399773/TB2rgtke9iK.eBjSZFsXXbxZpXa_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i3/1880505035634435666/TB2bToNiHXlpuFjSszfXXcSGXXa_!!0-saturn_solar.jpg
https://asearch.alicdn.com/bao/uploaded/i4/1519305020726924733/TB2I2VuhNhmpuFjSZFyXXcLdFXa_!!0-saturn_solar.jpg
`.split("\n")
  let i = 0
  setInterval(() => {
    if (i === urls.length) i = 0
    let img = new Image()
    img.src = urls[i++]
    document.body.prepend(img)
  }, 2000)
  onscroll = function(argument) {
    console.log("scrollY:" + scrollY)
  }
</script>

上面這個 demo 裡,假設我一直“追趕”的那張圖是“金凱瑞搖頭三人組”那張 GIF,那麼在 Chrome 56 之前的版本以及在其它的瀏覽器中,你看到的會是下面這樣的場景:

 

為了獲得更好的使用者體驗,Chrome 從 56 開始,開啟了一個叫做“滾動錨定(Scroll Anchoring)”的優化,效果就是,當頁面在視口上方的部分突然變高了 x 畫素,那麼瀏覽器會為你自動向下滾動 x 畫素,從而保證視口內容完全不變:

 

瀏覽器自動為你向下滾動 x 畫素,就意味著瀏覽器自己會觸發一次 scroll 事件,也意味著 scrollY 的值會增加 x,你可以通過上面的 demo 驗證這一點。

有些同學可能會有疑問,“這種場景多嗎?”、“我怎麼從來沒注意到?”、“有必要把事情搞複雜嗎?”。 從 Chrome 官方的統計可以看到,這個特性被觸發(替你滾動頁面)的概率大概為 1%,並不多,但也算不上是極端情況,所以優化還是有必要的。可能因為近些年網路條件越來越好,圖片載入的速度比你滾動頁面的速度還要快,所以不太容易遇到因網速慢導致的這類場景了(尤其在 WIFI 網路下)。

不過這個優化的確不是個簡單的改動,Chrome 從去年 3 月份開始實現這個特性,直到一年多後的今天,仍然有一些因這個優化導致的 bug 存在,這些 bug 多表現為頁面異常滾動,甚至像永動機一樣無限抖動,從這方面看,事情的確有一些被搞的複雜了。但幸好有一個 CSS 屬性可以關掉這個優化:overflow-anchor: none,你可以把這個屬性新增到發生 bug 的容器元素上,甚至加到 body 元素上也行,然後該元素及其它的所有後代節點就都不會被應用“滾動錨定”的優化了。除了作為瀏覽器 bug 的臨時 fix,我想不到其它使用這個屬性的場景了。

這個優化不僅限於看圖片的時候,任何元素節點,甚至文字節點也同樣適用。比如你在某新聞網站瀏覽一段文字的時候,視口上方突然非同步插入了一個未事先佔位的 iframe 廣告(微博輸入框下方就有這麼一個廣告),如果你使用了 Chrome 56 及以上版本的話,你完全察覺不到這一變化,你的閱讀不會被打斷。

頁面在視口上方的高度增加 x 畫素,瀏覽器會為你向下滾動 x 畫素;反過來,頁面在視口上方的高度減少 x 畫素,瀏覽器也會為你向上滾動 x 畫素,但這種情況更少見了。

該優化同樣適用於元素級別的滾動條,我也寫了一個 demo:

<style>
  div {
    width: 300px;
    height: 300px;
  }

  #container {
    background: red;
    overflow: scroll;
  }

  #aboveViewport {
    background: blue;
  }

  #anchorNode {
    background: green;
  }
</style>
<div id="container">
  向下滾動到底
  <div id="aboveAnchorNode"></div>
  <div id="anchorNode"></div>
  這段文字一旦出現就會始終在視口內
</div>
<script>
  let height = 100
  setInterval(() => {
    aboveAnchorNode.style.height = height += 10
  }, 1000)
</script>

由於本文講的是一個瀏覽器的優化,即便是前端開發者也沒有深究的必要,所以我故意省略了一些內容,比如什麼是錨定節點(anchor node )以及瀏覽器如何選定一個錨點節點?以及哪些樣式改動會把錨定節點擠出視口但不會觸發優化(Suppression Triggers),如果你想深究,可以從規範裡找到答案。

相關文章