Blink 系瀏覽器 History API scroll restoration 的 bug

Update 2016/04/02 15:30 Opera 36(.0.2130.46) 跟 Chrome 49 同步了,所以這個問題也消失了。此文章正式壽終正寢(?)

Update 2016/03/06 14:00 更新到 Chrome 49(.0.2623.75) 以後這個問題已經修正(bug & commit);而同樣是 Blink 系的 Opera 更新比較慢,在 Opera 35(.0.2066.92) 上還是可以重現問題。


最近寫的東西幾個主要的 view 都跟捲動位置有關,而捲動位置對網站瀏覽者的體驗事關重大。

本文內容針對桌面瀏覽器。

我覺得良好的捲動位置體驗是

  1. 在同頁面重新整理後會回到一樣的位置 (在內容物不變的前提之下)
  2. 承 (1),如果內容物在重新整理後會改變 (ex: Facebook),重新整理後可以乾脆回到頂端
  3. 使用瀏覽器的「上一頁」、「下一頁」進入頁面時會保持之前瀏覽的位置
  4. 曾經瀏覽過的頁面再次透過連結點擊進去後可以重置捲動位置(不需要有 3. 的行為)

上面這幾點基本上完全在現在瀏覽器的掌握之中,然而最近發現的 Blink 系瀏覽器(Chrome、Opera)在使用 History API 時的 scroll restoration 有點奇怪

  • 程式操作後的 scroll 並不會被記錄
  • 預設的捲軸 restoration 不適合一般 single page app

這兩點發生的一個重要前提 - 「在操作之後,使用者沒有再動過捲軸(拉動 scrollbar 或是用滾輪)」

程式操作後的 scroll 並不會被記錄

本文的兩個 demo 都是在 jsfiddle 上使用 history.pushState

首先第一個 demo (fork 自 https://jsfiddle.net/fJ9wq/)。

可以依照以下的操作重現問題:

  1. 在 P1 往下捲到某處 (<- 只有此步驟可以主動捲動,捲完稍微記一下位置)
  2. 點擊 P2
  3. 使用瀏覽器的 back 回到 P1
  4. 點擊頁面上方的「To Top」
  5. 點擊 P2 或 P3
  6. 使用瀏覽器的 back 回到 Page 1

(4) 之後 P1 的 scrollTop 應該是 0,因此合理的 scroll restoration 位置也是 0,然而在 (6) 時 P1 的捲軸位置會跟 (3) 時一樣。

關鍵在於上述重現問題的步驟裡只有 (1) 有讓使用者動到捲軸,其實中間只要在 (3) 回到 P1 之後使用者有主動操作捲動,後面 (6) 的現象就不會成立,而會是正常的結果。

預設的捲軸 restoration 不適合一般 single page app

針對使用 HTML5 History 的 single page app,它會

  • 在 document 的 height 的改動的情況下,不限次數地嘗試回復捲軸位置
  • 有時候會在不同頁面之間也嘗試回復同樣的捲軸位置

都是在使用者還沒有親自動捲軸的情況下。

我想這也可能是為什麼最近用 Chrome 開 Facebook 偶爾會有一些超可怕的瘋狂載入&捲動的原因。

這邊也用 jsfiddle 生了一個 demo,是 fork 上一個 demo 修改過來的,上方有增加顯示 scroll event 的觸發次數,可以觀察在問題發生的時候自動觸發的事件數量。為了模擬一些 single page app 的行為(例如: 動態文件高度),每次切換頁面會重新塞 10 張圖片進去,而圖片間刻意加了一些 delay,讓整個觸發的過程感覺比較明顯。

重現問題的操作:

  1. 在 P1 往下捲到某處 (<- 只有此步驟可以主動捲動,捲完稍微記一下位置)
  2. 前往 P2
  3. 使用瀏覽器的 back 回到 P1
  4. 透過點擊上方的 navigation 前往 P2

在 (3)、(4) 可以觀察到自動被多次觸發的 scroll event。而在 (4) 是第二次進入 P2,意外的竟然被自動捲到了跟前一頁 P1 一樣的捲動位置。

開發者角度的困難之處

就是要確切的控制捲動位置變得非常麻煩/困難。

回頭看看上面的問題,其中之一是它會在各種造成 document 高度改變之後再次自動捲動,這代表除非你完全掌握了你頁面載入的所有狀態(文章內容、圖片都已載入… etc),否則無法確實做到由開發者來指定 scroll 到某個位置。(例如: 在換頁後 1 秒開發者把捲動位置設為 0,2 秒後因為某張圖片載入完成,瀏覽器又再次自動回復捲動位置)

好在 Google 可能也有發現這東西實在太難以處理,在幾個月前推出了一個新的設定,可以把原生的 scroll restoration 關掉,暫時讓開發者可以擋一擋(X)。

History API: Scroll Restoration https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration

if ('scrollRestoration' in history) {
  // Back off, browser, I got this...
  history.scrollRestoration = 'manual';
}

其他瀏覽器

目前我只測試過 Firefox,對於上述的兩大類問題

  • 程式操作後的 scroll 並不會被記錄: 正常運作
  • 預設的捲軸 restoration 不太適合一般 single page app: 對於非同步的內容,Firefox 不會做特別的 scroll restoration 處理,所以開發者可以直接實作自行控制捲動位置

各位可以自己用上面的 demo 在其他不同瀏覽器上試試看。

後記

最近是在把某 React web app (掛 react-router) 從 hash history 改成 HTML5 history 之後,發現捲動位置整個爛了…

中間花了超多時間嘗試在沒有關掉原生的 scroll restoration 情況下,控制捲動位置;一個下午徒勞無功後才發現 scroll event 竟然會自動被多次觸發(我本來只把第一次擋下來之類的)。

這個問題真不是一般的麻煩,希望有緣人讀了文章之後能夠有所啟發,共勉之。

Update 2016/03/06 14:57 然後文章寫完那天發現 Chrome 49 把這個問題修掉了,我好恨啊~~~