深入瞭解新式網路瀏覽器 (第 4 部分)

Mariko Kosaka

輸入內容會傳送至合成器

這是 Chrome 內部檢視系列的最後一篇部落格文章,將探討 Chrome 如何處理我們的程式碼來顯示網站。在上一篇文章中,我們介紹了算繪程序,並說明瞭合成器。在本篇文章中,我們將探討在使用者輸入內容時,如何透過合成器提供流暢的互動體驗。

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

當您聽到「輸入事件」時,可能只會想到在文字方塊中輸入內容或滑鼠點擊,但從瀏覽器的角度來看,輸入是指使用者的任何手勢。滑鼠滾輪捲動是輸入事件,觸控或滑鼠游標移過也是輸入事件。

當使用者手勢 (例如輕觸螢幕) 發生時,瀏覽器程序會先收到手勢。不過,由於分頁內的內容是由算繪程序處理,因此瀏覽器程序只會知道手勢發生的位置。因此,瀏覽器程序會將事件類型 (例如 touchstart) 及其座標傳送至轉譯器程序。轉譯器程序會尋找事件目標並執行附加的事件監聽器,以便妥善處理事件。

輸入事件
圖 1:輸入事件透過瀏覽器程序轉送至轉譯器程序

合成器接收輸入事件

圖 2:檢視區懸停在頁面圖層上

在上一篇文章中,我們探討了合成器如何透過合成區塊化圖層,流暢地處理捲動作業。如果沒有輸入事件事件監聽器附加至網頁,合成器執行緒可以建立新的合成影格,完全不受主執行緒影響。但如果某些事件事件監聽器已附加至頁面,該怎麼辦?如何判斷事件是否需要處理?

瞭解非快速捲動區域

由於執行 JavaScript 是主執行緒的工作,因此在合成網頁時,合成器執行緒會將網頁中附有事件處理常式的區域標示為「非快速捲動區域」。有了這些資訊,合成器執行緒就能確保在該區域發生事件時,將輸入事件傳送至主執行緒。如果輸入事件來自這個區域以外,則合成器執行緒會繼續合成新影格,而不需要等待主執行緒。

限制非快速捲動區域
圖 3:對非快速捲動區域的輸入說明圖表

撰寫事件處理常式時請注意

網頁開發中常見的事件處理模式是事件委派。由於事件會向上傳遞,您可以在最上層元素中附加一個事件處理常式,並根據事件目標委派工作。您可能看過或編寫過類似以下的程式碼。

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault();
    }
});

由於您只需為所有元素撰寫一個事件處理常式,因此這個事件委派模式的人體工學設計相當吸引人。不過,如果您從瀏覽器的角度查看這段程式碼,現在整個網頁都會標示為無法快速捲動的區域。也就是說,即使應用程式不關心網頁特定部分的輸入內容,合成器執行緒仍必須與主執行緒通訊,並在每次輸入事件傳入時等待該執行緒。因此,編譯器的平順捲動功能會失效。

整個頁面無法快速捲動的區域
圖 4:對應到涵蓋整個網頁的非快速捲動區域的輸入說明圖表

為避免發生這種情況,您可以在事件事件監聽器中傳遞 passive: true 選項。這會向瀏覽器提示,您仍想在主執行緒中監聽事件,但合成器也可以繼續合成新影格。

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

檢查事件是否可取消

頁面捲動
圖 5:網頁的部分內容固定為水平捲動

假設您在網頁中使用了一個方塊,且希望限制其捲動方向僅限於水平捲動。

在指標事件中使用 passive: true 選項,表示頁面捲動可能會順暢,但在您想要 preventDefault 以限制捲動方向時,垂直捲動可能已經開始。您可以使用 event.cancelable 方法進行檢查。

document.body.addEventListener('pointermove', event => {
    if (event.cancelable) {
        event.preventDefault(); // block the native scroll
        /*
        *  do what you want the application to do here
        */
    }
}, {passive: true});

或者,您也可以使用 touch-action 等 CSS 規則,完全移除事件處理常式。

#area {
  touch-action: pan-x;
}

尋找事件目標

點擊測試
圖 6:主執行緒查看繪圖記錄,詢問在 x.y 點繪製了什麼

當合成器執行緒將輸入事件傳送至主執行緒時,首先要執行的是命中測試,以便找出事件目標。命中測試會使用在算繪程序中產生的繪圖記錄資料,找出事件發生點座標下方的內容。

將事件調度降到主要執行緒

在上一篇文章中,我們討論了一般螢幕每秒刷新 60 次的情形,以及我們如何保持節奏,以便呈現流暢的動畫。就輸入而言,一般觸控螢幕裝置每秒會傳送 60 到 120 次觸控事件,一般滑鼠則會傳送 100 次事件。輸入事件的精確度高於螢幕的更新頻率。

如果 touchmove 等持續性事件每秒傳送至主執行緒 120 次,則可能會觸發過多命中測試和 JavaScript 執行作業,導致螢幕更新速度變慢。

未經篩選的事件
圖 7:事件大量湧入影格時間軸,導致網頁卡頓

為盡量減少對主執行緒的過度呼叫,Chrome 會合併連續事件 (例如 wheelmousewheelmousemovepointermovetouchmove),並延遲調度,直到下一個 requestAnimationFrame 前。

合併事件
圖 8:與前述相同的時間軸,但事件會合併及延遲

任何離散事件 (例如 keydownkeyupmouseupmousedowntouchstarttouchend) 都會立即調度。

使用 getCoalescedEvents 取得影格內事件

對於大多數的網頁應用程式而言,合併事件應該足以提供良好的使用者體驗。不過,如果您要建立繪圖應用程式,並根據 touchmove 座標放置路徑,可能會遺漏中間座標,無法繪製平滑的線條。在這種情況下,您可以在指標事件中使用 getCoalescedEvents 方法,取得這些合併事件的相關資訊。

getCoalescedEvents
圖 9:左側為平滑觸控手勢路徑,右側為合併的限制路徑
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.
    }
});

後續步驟

在本系列文章中,我們介紹了網路瀏覽器的內部運作方式。如果您從未想過為何 DevTools 建議您在事件處理常式中加入 {passive: true},或是為何要在指令碼標記中寫入 async 屬性,希望本系列文章能讓您瞭解瀏覽器為何需要這些資訊,才能提供更快速、更流暢的網路體驗。

使用 Lighthouse

如果您想讓程式碼適合瀏覽器,但不知道從何處著手,不妨使用Lighthouse 這項工具,它可以對任何網站執行稽核,並提供報告,說明哪些做法正確,哪些需要改善。閱讀稽核清單,也可以讓您瞭解瀏覽器重視哪些項目。

瞭解如何評估成效

不同網站的效能調整方式可能不同,因此您必須評估網站成效,並決定最適合網站的做法。Chrome 開發人員工具團隊提供幾個教學課程,說明如何評估網站成效

在網站中新增功能政策

如果您想採取額外步驟,功能政策是新的網路平台功能,可在您建構專案時提供防護機制。開啟功能政策可確保應用程式的特定行為,並避免您犯錯。舉例來說,如果您想確保應用程式絕不會阻斷剖析作業,可以讓應用程式採用同步指令碼政策。啟用 sync-script: 'none' 後,系統就會禁止執行解析器封鎖 JavaScript。這樣一來,任何程式碼都不會阻斷剖析器,瀏覽器也不必擔心會暫停剖析器。

總結

謝謝

開始建構網站時,我幾乎只在乎如何編寫程式碼,以及如何提高工作效率。這些都是重要的考量,但我們也應思考瀏覽器如何處理我們撰寫的程式碼。現代瀏覽器一直在努力為使用者提供更優質的網路體驗。透過整理程式碼,讓瀏覽器運作順暢,進而提升使用者體驗。希望您能與我一起努力,讓瀏覽器更友善!

非常感謝所有審查本系列早期草稿的人員,包括 (但不限於) Alex RussellPaul IrishMeggin KearneyEric BidelmanMathias BynensAddy OsmaniKinuko YasudaNasko Oskov 和 Charlie Reis。

您喜歡這個系列嗎?如有任何問題或建議,歡迎在下方的留言區留言,或在 Twitter 上傳送訊息給 @kosamari