讓頁面滑動流暢得飛起的新特性:Passive Event Listeners

版權聲明:本文由陳志興原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/153html

來源:騰雲閣 https://www.qcloud.com/communitygit

 

在不久前的Google I/O 2016 Mobile Web Talk中,Google公佈了一個讓頁面滑動更流暢的新特性Passive Event Listeners。該特性目前已經集成到Chrome51版本中。

Chrome51上使用Passive Event Listener特性先後的效果對比 連接地址
從效果對比視頻中能夠明顯看到,使用Passive Event Listeners特性後,頁面的滑動流暢度相對使用以前提高了不少。github

看完Passive Event Listeners特性這麼給力的效果後,相信大部分童鞋腦海中都會產生如下幾個問題:瀏覽器

  1. Passive Event Listeners是什麼?多線程

  2. 爲何須要Passive Event Listeners?併發

  3. Passive Event Listeners是怎麼實現的?框架

接下來,咱們將圍繞上面的這3個問題來深刻理解Passive Event Listeners特性。ide

Passive Event Listeners是什麼?

Passive event listeners are a new feature in the DOM spec that enable developers to opt-in to better scroll performance by eliminating the need for scrolling to block on touch and wheel event listeners. Developers can annotate touch and wheel listeners with {passive: true} to indicate that they will never invoke preventDefault.函數

Passive Event Listeners是Chrome提出的一個新的瀏覽器特性:Web開發者經過一個新的屬性passive來告訴瀏覽器,當前頁面內註冊的事件監聽器內部是否會調用preventDefault函數來阻止事件的默認行爲,以便瀏覽器根據這個信息更好地作出決策來優化頁面性能。當屬性passive的值爲true的時候,表明該監聽器內部不會調用preventDefault函數來阻止默認滑動行爲,Chrome瀏覽器稱這類型的監聽器爲被動(passive)監聽器。目前Chrome主要利用該特性來優化頁面的滑動性能,因此Passive Event Listeners特性當前僅支持mousewheel/touch相關事件。佈局

以下面的Html代碼中,頁面經過調用document.addEventListener來添加一個mousewheel事件的監聽器handler,並經過設置passive屬性的值爲true來聲明監聽器handler是被動監聽mousewheel事件,即handler內部不會調用事件的preventDefault函數。

爲何須要Passive Event Listeners特性?

Passive Event Listeners特性是爲了提升頁面的滑動流暢度而設計的,頁面滑動流暢度的提高,直接影響到用戶對這個頁面最直觀的感覺。這個不難理解,想象一下你想要滑動某個頁面瀏覽內容,當你用鼠標滾輪或者用手指觸摸屏幕上下滑動的時候,頁面並無按你的預期進行滾動,此時你心裏每每會感受到一絲不爽,甚至想放棄該頁面。Facebook以前作了一項試驗,他們將頁面滑動的響應刷新率從60FPS下降到30FPS的時候,發現用戶的參與度急速降低。

由前面對Passive Event Listeners特性的介紹可知,Passive Event Listenrers特性是讓Web開發者來告訴瀏覽器,當前頁面內註冊的mousewheel/touch事件監聽器是否屬於被動監聽器,以便讓瀏覽器更好地作決策來提升頁面的滑動流暢度。那麼Chrome瀏覽器爲何須要知道是否被動監聽器這個信息呢?瀏覽器知道這個信息以後,它要作什麼決策呢?要回答這個問題,有必要先了解一下目前Chrome瀏覽器的線程化渲染框架,它是Passive Event Listeners特性的基礎。

在介紹Chrome瀏覽器的線程化渲染框架以前,咱們先來簡單瞭解本文涉及到的Chrome瀏覽器的一些概念。

  1. 繪製(Paint):將繪製操做轉換成爲圖像的過程(好比軟件模式下通過光柵化生成位圖,硬件模式下通過光柵化生成紋理)。在Chrome中,繪製分爲兩部分實現:繪製操做記錄部分(main-thread side)和繪製實現部分(impl-side)。繪製記錄部分將繪製操做記錄到SKPicture中,繪製實現部分負責將SKPicture進行光柵化轉成圖像;

  2. 圖層(Paint Layer):在Chrome中,頁面的繪製是分層繪製的,頁面內容變化的時候,瀏覽器僅須要從新繪製內容變化的圖層,沒有變化的圖層不須要從新繪製;

  3. 合成(Composite):將繪製好的圖層圖像混合在一塊兒生成一張最終的圖像顯示在屏幕上的過程;

  4. 渲染(Render):繪製+合成=渲染;

  5. UI線程(UI Thread):瀏覽器的主線程,負責接收到系統派發給瀏覽器窗口的事件,資源下載等;

  6. 內核線程(Main/Render Thread):Blink內核及V8引擎運行的線程,如DOM樹構建,元素佈局,繪製(main-thread side),JavaScript執行等邏輯在該線程中執行;

  7. 合成線程(Compositor Thread):負責圖像合成的線程,如繪製(impl-side),合成等邏輯在該線程中執行。

OK,瞭解完上面的幾個概念後,咱們正式開始Chrome線程渲染框架的介紹。

Chrome瀏覽器的線程化渲染框架

咱們回顧一下傳統的單線程渲染框架,以下圖所示,內核線程幾乎包攬了頁面內容渲染的全部工做,如JavaScript執行,元素佈局,圖層繪製,圖層圖像合成等,每項工做的執行耗時基本都跟頁面內容相關,耗時通常在幾十毫秒至幾百毫秒不等。

對於這種單線程渲染框架,存在兩個明顯的問題:

  1. 流水線的執行方式,後面的工做必須等待前面工做執行完成才能處理,沒法將相互獨立的工做並行處理;

  2. 內核線程負責的工做太多且耗時,一旦趕上內核在執行耗時較長的工做,用戶的輸入事件是將沒法當即獲得響應的。

對於第1個問題,瀏覽器很難控制頁面從內容變化到佈局渲染整個過程的耗時(即新生成一幀內容的耗時),中間任何一項工做的執行均可能致使總體過程耗時變大,過大的耗時會致使頁面內容的刷新率偏低,從而造成視覺上的卡頓。如瀏覽器收到VSync中斷信號通知的時候,意味着頁面須要當即對內容進行渲染,但這個時候內核線程可能還在執行一些業務的JavaScript代碼,致使頁面內容的渲染沒法當即開始,若是頁面沒法在下一個VSync中斷信號到來以前完成對內容的渲染,則頁面會出現丟幀,用戶感受到頁面操做出現卡頓。

注:VSync信號中斷的頻率,通常跟設備屏幕的刷新率對齊,好比設備的刷新率爲60FPS(Frames Per Second),那麼大概16.67ms會觸發一下Vsync中斷信號。Chrome瀏覽器和Android系統等都是經過VSync中斷信號來通知頁面啓動內容的渲染(BeginFrame)。

對於第2個問題,因爲內核線程負責的工做太多,這將致使內核線程常常處於忙碌狀態,沒法快速處理外界的輸入消息,表現爲用戶操做了頁面,可是沒法當即獲得響應。

爲了優化第1個問題,Chrome瀏覽器對內核線程負責的工做進行拆分,經過多線程併發處理提升渲染效率減小丟幀,如內核線程僅負責DOM樹構建、元素的佈局、圖層繪製記錄部分(main-thread side)、JavaScript的執行,而圖層繪製實現部分(impl-side)、圖層圖像合成則是交給合成線程負責處理。這種多線程負責頁面內容的渲染的框架,在Chrome中稱爲線程化渲染框架(Threaded Compositor Architecture)。

如上圖所示,在Chrome的線程化渲染框架中,當內核線程完成第1幀(Frame#1)的佈局和記錄繪製操做,當即通知合成線程對第一幀(Frame#1)進行渲染,而後內核線程就開始準備第2幀(Frame#2)的佈局和記錄繪製操做。由此能夠看出,內核線程在進行第N+1幀的佈局和記錄繪製操做同時,合成線程也在努力進行第N幀的渲染並交給屏幕展現,這裏利用了CPU多核的特性進行併發處理,所以提升了頁面的渲染效率。由此也可知,實際上用戶看到的頁面內容,是上一幀的內容快照,新的一幀還在處理中。

要優化第2個問題,對瀏覽器來講很是困難的。只要輸入事件要在內核線程執行邏輯,那麼遇到內核線程在忙,必然沒法當即獲得響應。如用戶的大部分輸入事件都跟頁面元素有關係,一旦頁面元素註冊了對應事件的監聽器,監聽器的邏輯代碼(JavaScript)必須在內核線程中執行(V8引擎是運行在內核線程),所以這種輸入事件常常沒法當即獲得響應的。

由上面的分析知道,用戶的輸入事件沒法當即獲得響應,是由於須要派發給內核線程處理。那有沒有一些輸入事件是能夠不通過內核線程就能被快速處理的呢?答案是確定的。

在Chrome中,這類能夠不通過內核線程就能快速處理的輸入事件爲手勢輸入事件(滑動、捏合),手勢輸入事件是由用戶連續的普通輸入事件組合產生,如連續的mousewheel/touchmove事件可能會生成GestureScrollBegin/GestureScrollUpdate等手勢事件。手勢輸入事件能夠直接在已經渲染好的內容快照上操做,如滑動手勢事件,直接對頁面已經渲染好的內容快照進行滑動展現便可。因爲線程化渲染框架的支持,手勢輸入事件能夠不通過內核線程,直接由合成線程在內容快照上直接處理,因此即便此時內核線程在忙碌,用戶的手勢輸入事件也是能夠立刻獲得響應的。你們能夠搞一個簡單的demo驗證一下Chrome瀏覽器的這個特性:如在一個有滾動條的頁面內經過JavaScript執行一段死循環的代碼(while-true之類的),這個時候再去嘗試上下滑動頁面,你會發現此時頁面仍能流暢地滑動。

由此可知,Chrome瀏覽器對於手勢輸入事件的響應是很是快的,由於它能夠不須要通過內核線程,直接由合成線程快速處理。然而手勢輸入事件的產生可能須要內核線程,這會致使Chrome對手勢輸入事件的優化效果大打折扣。由前面介紹知道,手勢輸入事件是由連續的普通輸入事件組成,而這些普通的輸入事件可能會被對應的事件監聽器內部調用preventDefault函數來阻止掉事件的默認行爲,在這種場景下是不會產生手勢輸入事件。如連續的mousewheel事件默承認以產生GestureScrollUpdate事件,可是若是監聽器內部調用了preventDefault函數,那麼這種狀況下則不該該產生GestureScrollUpdate手勢事件的。瀏覽器只有等內核線程執行到事件監聽器對應的JavaScript代碼時,才能知道內部是否會調用preventDefault函數來阻止事件的默認行爲,因此瀏覽器自己是沒有辦法對這種場景進行優化的。這種場景下,用戶的手勢事件沒法快速產生,會致使頁面沒法快速執行滑動邏輯,從而讓用戶感受到頁面卡頓。

而Chrome團隊從統計數據中分析得出,註冊了mousewheel/touch相關事件監聽器的頁面中,80%的頁面內部都不會調用preventDefault函數來阻止事件的默認默認行爲。對於這80%的頁面,即便監聽器內部什麼都沒有作,相對沒有註冊mousewheel/touch事件監聽器的頁面,在滑動流暢度上,有10%的頁面增長至少100ms的延遲,1%的頁面甚至增長500ms以上的延遲。Chrome團隊認爲對於統計中的這80%的頁面來講,他們都是不但願由於註冊mousewheel/touch相關事件監聽器而致使滑動延遲增長的。點擊這裏 能夠體驗頁面註冊後致使的滑動延遲,如上圖。

若是能讓Web開發者來明確告訴瀏覽器,監聽器內部不會調用preventDefault函數來禁止默認的事件行爲,那麼瀏覽器將能快速生成手勢輸入事件,從而讓頁面響應更快。

介紹完這裏,你們應該明白Chrome瀏覽器爲何須要Passive Event Listeners特性了。接下來,咱們來看看Passive Event Listeners特性是怎麼實現的。

Passive Event Listeners的實現


爲了更好地理解Passive Event Listeners特性,咱們接下來了解一下它的實現過程。如上面代碼所示,假定頁面中註冊了mousewheel事件的被動監聽器,此時用戶開始滑動鼠標滾輪來滑動頁面。

如上圖所述,用戶的鼠標滾輪事件(WM_MouseWheel)由操做系統內核捕捉後,操做系統會將該事件派發給瀏覽器的UI線程處理。UI線程內部將系統的WM_MouseWheel事件轉換爲Chrome的WebInputEvent::MouseWheel事件後,接着經過IPC通道派發給合成線程的輸入事件處理器處理。

合成線程的輸入事件處理器收到WebInputEvent::MouseWheel事件後,內部先會查詢MouseWheel事件監聽器的類型屬性,而後根據監聽器的類型屬性值來進行不一樣邏輯的處理。

目前Chrome中監聽器的類型屬性值主要有四種:EventListenerProperties::kNone,EventListenerProperties::kPassive,EventListenerProperties::kBlocking,EventListenerProperties::kBlockingAndPassive,以下代碼所述。

在Chrome中,kBlocking和kBlockingAndPassive類型屬性的處理邏輯是同樣的,這個不難理解,只要存在一個非passive類型的事件監聽器,那麼都有可能阻止事件的默認行爲。接下來,咱們瞭解一下不一樣類型屬性監聽器的實現邏輯。

場景1: EventListenerProperties::kNone類型

當事件監聽器的類型屬性爲EventListenerProperties::kNone時,意味着當前頁面內沒有註冊對應事件的監聽器。對於這種場景(如上圖中的MouseWheel Handlers:No分支),合成線程會立刻發送一個MouseWheel的ACK消息給UI線程,UI線程收到MouseWheel的ACK消息後,會判斷該事件是否被消費(Comsumed,即調用了preventDefault),若是已經被消費,則什麼都不作。不然,UI線程會產生一個滑動手勢事件(若是當前不是在滑動過程,手勢事件爲GestureScrollBegin,不然爲GestureScrollUpdate),並滑動手勢事件經過IPC通道派發給合成線程處理,合成線程收到該滑動手勢事件以後,直接對內容快照進行滑動處理,並展現給到屏幕上。這種場景下,因爲沒有涉及到內核線程處理,用戶的輸入響應會很是及時。

場景2: EventListenerProperties::kBlockingAndPassive 或 cc::EventListenerProperties::kBlocking類型

當事件監聽器的類型屬性爲EventListenerProperties::kBlockingAndPassive或EventListenerProperties::kBlocking時,意味着當前頁面至少存在一個非passive類型的事件監聽器。對應這種場景(如上圖中的MouseWheel Handlers:YES-Passive:No分支),合成線程沒法知道對應的監聽器內部是否會調用preventDefault函數來阻止默認行爲,此時合成線程只能將該輸入事件派發給內核線程處理(Dispatch Event to Main Thread)。等內核線程執行完監聽器的處理邏輯後(Run JS Handler),再發送一個MouseWheel的ACK消息給UI線程,UI線程收到Mouse Wheel的ACK消息後的處理邏輯跟場景1一致。這種場景下,手勢輸入事件必須等待事件監聽器邏輯處理完成後纔會產生並派發給合成線程處理,因爲事件監聽器邏輯的執行時機不肯定,將很是容易致使用戶的輸入事件沒法當即響應。

場景3: EventListenerProperties::kPassive類型

當事件監聽器的類型屬性爲EventListenerProperties::kPassive時,意味着當前頁面只存在passive類型的事件監聽器。對於這種場景(如上圖中的MouseWheel Handlers:YES-Passive:YES分支),合成線程首先會發送一個MouseWheel的ACK消息給UI線程,執行跟場景1中同樣的邏輯,同時將該事件派發給內核線程處理,執行跟場景2類似的邏輯,可是在Run JS Handlers完成後,不會再發送Mouse Wheel事件的ACK消息。這種場景下,其實是場景2和場景3的組合,兩個場景是並行處理的,所以用戶的MouseWheel輸入事件能會被馬上響應,也不會受到內核線程的事件監聽器處理邏輯影響。

對於場景1和場景3的滑動,在Chrome中稱爲fast scroll模式,而場景2則稱爲slow scroll模式。

總結

通過上面的分析,咱們瞭解到了Passive Event Listeners特性是什麼,Passive Event Listeners特性產生的背景及Passive Event Listeners特性的實現邏輯,這其中涉及到了Chrome的多線程渲染框架、輸入事件處理等知識。

相關文章
相關標籤/搜索