窺探現代瀏覽器架構(四)

前言

本文是筆者對Mario Kosaka寫的inside look at modern web browser系列文章的翻譯。這裏的翻譯不是指直譯,而是結合我的的理解將做者想表達的意思表達出來,並且會盡可能補充一些相關的內容來幫助你們更好地理解。javascript

到達合成線程的輸入

這篇文章是探究Chrome內部工做原理的四集系列文章中的最後一篇了。在上一篇文章中,咱們探討了一下瀏覽器渲染頁面的過程以及學習了一些關於合成線程的知識,在本篇文章中,咱們要看一下當用戶在網頁上輸入內容的時候,合成線程(compositor)作了些什麼來保證流暢的用戶體驗的。java

從瀏覽器的角度來看輸入事件

當你聽到「輸入事件」(input events)的時候,你可能只會想到用戶在文本框中輸入內容或者對頁面進行了點擊操做,但是從瀏覽器的角度來看的話,輸入其實表明着來自於用戶的任何手勢動做(gesture)。因此用戶滾動頁面觸碰屏幕以及移動鼠標等操做均可以看做來自於用戶的輸入事件。git

當用戶作了一些諸如觸碰屏幕的手勢動做時,瀏覽器進程(browser process)是第一個能夠接收到這個事件的地方。但是瀏覽器進程只能知道用戶的手勢動做發生在什麼地方而不知道如何處理,這是由於標籤內(tab)的內容是由頁面的渲染進程(render process)負責的。所以瀏覽器進程會將事件的類型(如touchstart)以及座標(coordinates)發送給渲染進程。爲了能夠正確地處理這個事件,渲染進程會找到事件的目標對象(target)而後運行這個事件綁定的監聽函數(listener)。github

點擊事件從瀏覽器進程路由到渲染進程web

合成線程接收到輸入事件

在上一篇文章中,咱們查看了合成線程是如何經過合併頁面已經光柵化好的層來保障流暢滾動體驗(scroll smoothly)的。若是當前頁面不存在任何用戶事件的監聽器(event listener),合成線程徹底不須要主線程的參與就能建立一個新的合成幀來響應事件。但是若是頁面有一些事件監聽器(event listeners)呢?合成線程是如何判斷出這個事件是否須要路由給主線程處理的呢?chrome

瞭解非快速滾動區域 - non-fast scrollable region

由於頁面的JavaScript腳本是在主線程(main thread)中運行的,因此當一個頁面被合成的時候,合成線程會將頁面那些註冊了事件監聽器的區域標記爲「非快速滾動區域」(Non-fast Scrollable Region)。因爲知道了這些信息,當用戶事件發生在這些區域時,合成線程會將輸入事件發送給主線程來處理。若是輸入事件不是發生在非快速滾動區域,合成線程就無須主線程的參與來合成一個新的幀。瀏覽器

非快速滾動區域有用戶事件發生時的示意圖架構

當你寫事件監聽器的時候留點心眼

Web開發的一個常見的模式是事件委託(event delegation)。因爲事件會冒泡,你能夠給頂層的元素綁定一個事件監聽函數來做爲其全部子元素的事件委託者,這樣子節點的事件就能夠統一被頂層的元素處理了。所以你可能看過或者寫過相似於下面的代碼:async

document.body.addEventListener('touchstart', event => {
  if (event.target === area) {
    event.preventDefault()
  }
})
複製代碼

只用一個事件監聽器就能夠服務到全部的元素,乍一看這種寫法仍是挺實惠的。但是,若是你從瀏覽器的角度去看一下這段代碼,你會發現上面給body元素綁定了事件監聽器後實際上是將整個頁面都標記爲一個非快速滾動區域,這就意味着即便你頁面的某些區域壓根就不在意是否是有用戶輸入,當用戶輸入事件發生時,合成線程每次都會告知主線程而且會等待主線程處理完它才幹活。所以這種狀況下合成線程就喪失提供流暢用戶體驗的能力了(smooth scrolling ability)。ide

當整個頁面都是非快速滾動區域時頁面的事件處理示意圖

爲了減輕這種狀況的發生,您能夠爲事件監聽器傳遞passive:true選項。 這個選項會告訴瀏覽器您仍要在主線程中偵聽事件,但是合成線程也能夠繼續合成新的幀。

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});
複製代碼

查找事件的目標對象(event target)

當合成線程向主線程發送輸入事件時,主線程要作的第一件事是經過命中測試(hit test)去找到事件的目標對象(target)。具體的命中測試流程是遍歷在渲染流水線中生成的繪畫記錄(paint records)來找到輸入事件出現的x, y座標上面描繪的對象是哪一個。

主線程經過遍歷繪畫記錄來肯定在x,y座標上的是哪一個對象

最小化發送給主線程的事件數

上一篇文章中咱們有說過顯示器的刷新頻率一般是一秒鐘60次以及咱們能夠經過讓JavaScript代碼的執行頻率和屏幕刷新頻率保持一致來實現頁面的平滑動畫效果(smooth animation)。對於用戶輸入來講,觸摸屏通常一秒鐘會觸發60到120次點擊事件,而鼠標通常則會每秒觸發100次事件,所以輸入事件的觸發頻率其實遠遠高於咱們屏幕的刷新頻率。

若是每秒將諸如touchmove這種連續被觸發的事件發送到主線程120次,由於屏幕的刷新速度相對來講比較慢,它可能會觸發過量的點擊測試以及JavaScript代碼的執行。

事件淹沒了屏幕刷新的時間軸,致使頁面很卡頓

爲了最大程度地減小對主線程的過多調用,Chrome會合並連續事件(例如wheelmousewheelmousemovepointermovetouchmove),並將調度延遲到下一個requestAnimationFrame以前。

和以前相同的事件軸,但是此次事件被合併並延遲調度了

任何諸如keydownkeyupmouseupmousedowntouchstarttouchend等相對不怎麼頻繁發生的事件都會被當即派送給主線程。

使用getCoalesecedEvents來獲取幀內(intra-frame)事件

對於大多數web應用來講,合併事件應該已經足夠用來提供很好的用戶體驗了,然而,若是你正在構建的是一個根據用戶的touchmove座標來進行繪圖的應用的話,合併事件可能會使頁面畫的線不夠順暢和連續。在這種狀況下,你可使用鼠標事件的getCoalescedEvents來獲取被合成的事件的詳細信息。

左邊是順暢的觸摸手勢,右邊是事件合成後不那麼連續的手勢

window.addEventListener('pointermove', event => {
    const events = event.getCoalescedEvents();
    for (let event of events) {
        const x = event.pageX;
        const y = event.pageY;
        // draw a line using x and y coordinates.
    }
});
複製代碼

下一步

這本系列的文章中,咱們以Chrome瀏覽器爲例子探討了瀏覽器的內部工做原理。若是你以前歷來沒有想過爲何DevTools推薦你在事件監聽器中使用passive:true選項或者在script標籤中寫async屬性的話,我但願這個系列的文章能夠給你一些關於瀏覽器爲何須要這些信息來提供更快更流暢的用戶體驗的緣由。

學習如何衡量性能

不一樣網站的性能調整可能會有所不一樣,你要本身衡量本身網站的性能並肯定最適合提高你的網站性能的方案。 你能夠查看Chrome DevTools團隊的一些教程來學習如何才能衡量本身網站的性能

爲你的站點添加Feature Policy

若是你想更進一步,你能夠了解一下Feature Policy這個新的Web平臺功能,這個功能能夠在你構建項目的時候提供一些保護讓您的應用程序具備某些行爲並防止你犯下錯誤。例如,若是你想確保你的應用代碼不會阻塞頁面的解析(parsing),你能夠在同步腳本策略(synchronius scripts policy)中運行你的應用。具體作法是將sync-script設置爲'none',這樣那些會阻塞頁面解析的JavaScript代碼會被禁止執行。這樣作的好處是避免你的代碼阻塞頁面的解析,並且瀏覽器無須擔憂解析器(parser)暫停。

總結

以上就是全部和瀏覽器架構和運行原理相關的內容了,咱們之後在開發web應用的時候,不該該只考慮代碼的優雅性,還要多多從瀏覽器是如何解析運行咱們的代碼的方面進行思考,從而爲用戶提供更好的用戶體驗。

持續關注個人技術動態

我是進擊的大蔥,關注我和我一塊兒進步成獨當一面的全棧工程師!

文章首發於:窺探現代瀏覽器架構(四)

關注個人我的公衆號獲取個人最新技術推送!

相關文章
相關標籤/搜索