深入瞭解新式網路瀏覽器 (第 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});

或者,您也可以使用 CSS 規則 (例如 touch-action),徹底刪除事件處理常式。

#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