R·ex / Zeng


音遊狗、安全狗、攻城獅、業餘設計師、段子手、苦學日語的少年。

由於瀏覽器的最佳化導致的 Bug 們

按理說,隨著時間的發展,瀏覽器會做各種各樣的最佳化來提升網頁瀏覽的效能,但不是所有最佳化都能起到積極的效果,有的最佳化甚至屬於“智障”級別。下面我就分享三個我遇到或聽說的、由於瀏覽器自身的最佳化導致的一些 Bug。

Passive Event Listeners

我之所以知道了這個東西,是因為昨天鄰居向我抱怨的一個問題,他的移動端頁面使用了一個日期選擇器,但在上下滑動選擇日期的時候,Console 裡面會報一堆這樣的錯誤:

[Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080

“Passive”這個詞,一開始我還以為是“由程式碼觸發的”監聽器,直到我去網上搜了一圈才知道,這是一個開始於 Chrome 51 的特性,旨在提升頁面滑動的流暢度。大概的理解是(摘自 SegmentFault,有修改):

當你觸控滑動頁面時,頁面應該跟隨手指一起滾動。如果你綁定了一個觸控事件(大概執行 200 毫秒),瀏覽器就犯迷糊了:如果你在事件繫結函式中呼叫了 preventDefault,那麼頁面就不應該滾動;但是瀏覽器一開始不知道你有沒有呼叫,只能先執行你的函式,等 200 毫秒後,瀏覽器才知道,“哦,原來你沒有阻止預設行為,好的我馬上滾”。此時,頁面開始滾。

經統計,80% 左右的觸控事件都沒有被 preventDefault 阻止,這些等待的時間都被浪費掉了。使用 Passive Event Listeners,開發者能夠提前告訴瀏覽器:“我不呼叫 preventDefault 函式來阻止事件預設行為”,那麼瀏覽器就無需等待事件執行完成,而是直接執行預設行為(滾動)。

一開始在 Chrome 瀏覽器中,如果發現耗時超過 100 毫秒的非 Passive 的監聽器,會在 DevTools 裡面警告你:需要把 addEventListener 的第三個引數設為 {passive: true};但新版 Chrome 有一個實驗性的功能:Passive Event Listener Override(chrome://flags/#passive-listener-default),目的是將 touchstarttouchmovemousewheel 等事件當作 Passive 事件,而且這個功能是預設開啟的,因此在新版 Chrome 中,如果你沒有顯性的使用 dom.addEventListener(type, fn, { passive: false }) 的話,你在這些事件中的 preventDefault 不光沒有用,還會報個錯。

雖然解決方法很簡單,但很討厭的一點就是,這個特性並不是在某次重大更新中釋出的,除非你每天盯著 Chrome Release Note,否則很容易被忽視掉;而且它打破了之前的規則,導致你的網站在某一天突然收到客戶的 Bug 反饋。

Firefox Quantum 的並行引擎

這個坑主要是我的原因。在上古時期,我需要在輸入框內容改變的時候,實時獲取其值(change 無法滿足實時的條件),當時還沒有 input 事件,因此我的方法是這樣的:

input.addEventListener('keydown', function () {
    setTimeout(function () {
        console.log(input.value);
    }, 0);
});

雖然 keydown 觸發的時候 DOM 還沒更新,此時獲取 input.value 會得到舊值,但經過 setTimeout 包了一層,於是用來獲取的程式碼被放到了下一個事件迴圈中,此時 input.value 已經改變了。這個寫法在 2017 年 11 月前的最新瀏覽器中都是沒問題的。

然而 2017 年 11 月釋出的 Firefox Quantum 使用了 Servo 作為瀏覽器引擎,後者利用了 CPU 多核的優勢,讓 DOM 的更新脫離了 JavaScript 的事件迴圈。這會提高渲染的速度,但同時會導致我的程式碼失效,因為此時無法保證下一個事件迴圈中 input.value 究竟有沒有變化。

解決方法很簡單,使用 HTML5 的 input 事件即可。不過我總覺得,如果大家不知道 DOM 的更新已經脫離了事件迴圈,以後可能會出現其它類似的問題……

Webkit CSS3 合成層的災難

這是我偶然從一篇文章裡面看到的。文章連結在本文最下方,由於七牛回收了測試域名,文章中的圖片已經失效了。

人們常說移動端要想動畫效能流暢,應該使用硬體加速,但人們還常說“最佳化沒有銀彈”,一味的使用硬體加速反而會降低效能。

Webkit 中的硬體加速原理是:把需要渲染的元素放到特定的合成層,這些層裡面的內容是靜態的,因此 Webkit 可以將其作為紋理上傳到 GPU,以提高渲染速度。這個技術對於前端來說,通常是用於避免當某個元素的某些屬性改變時重繪頁面,因為如果它自成一層,就可以把其它元素搞成紋理,再整體渲染;但如果合成層的數量過多,那麼需要繪製的紋理就太多,這相當於沒做什麼最佳化。

一般來說,有經驗的開發者會注意到這一點,因此不會使用太多的 transform: translateZ(0) 來啟用硬體加速。但其實 Webkit 的“層建立標準”還真不少,而且滿足任一標準即可建立層(未來標準可能會改變):

  • 3D 或透視變換(perspective transform)CSS 屬性
  • 使用加速影片解碼的 <video> 元素
  • 擁有 3D (WebGL) 上下文或加速的 2D 上下文的 <canvas> 元素
  • 混合外掛(如 Flash)
  • 對自己的 opacity 做 CSS 動畫,或使用一個動畫變換的元素
  • 擁有加速 CSS 過濾器的元素
  • 元素有一個包含合成層的後代節點(換句話說,就是一個元素擁有一個子元素,該子元素在自己的層裡)
  • 元素有一個 z-index 較低且包含一個合成層的兄弟元素(換句話說就是該元素在合成層上面渲染)

注意最後一句,這意味著,如果你沒有給“可能自成一層”的元素設定一個比較大的 z-inedx,瀏覽器有可能會給“所有跟它父元素相同的相對或絕對定位的元素”都建立一個合成層來渲染……

這特麼不是智障是什麼?!

所以如果懷疑頁面滾動出了問題,或想做一些效能最佳化,可以先看看是不是這兒的問題,除錯的方法是在 Chrome 中開啟“Show composited layer borders”,控制檯和 Flags 裡面都可以開。如果發現頁面中的黃色邊框太多,那說明你的合成層太多了,如果有些元素本來沒必要開啟合成層,但卻被黃色邊框框起來了,那你可能需要檢查一下 CSS 程式碼了。


參考文章

Disqus 載入中……如未能載入,請將 disqus.com 和 disquscdn.com 加入白名單。

這是我們共同度過的

第 3853 天