「前端進階必備」深入理解現(xiàn)代瀏覽器
編者按:本文作者李松峰,資深技術圖書譯者,翻譯出版過40余部技術及交互設計專著,現(xiàn)任360奇舞團Web前端開發(fā)資深專家,360前端技術委員會委員、W3C AC代表。
各位,如果你的職業(yè)是開挖掘機,你說要不要深入理解挖掘機?通常來說,深入理解你操縱的機器才能最終達到人機一體的境界。
當然,你可以說:不用,因為如果挖掘機不好使,我可以換一臺。嗯,也有道理。不過,假如你同時又是一名前端開發(fā)者,那你要不要深入理解瀏覽器呢?注意,身為前端,你不太可能有機會因為瀏覽器不好使就強迫用戶換一個你認為好使的。這時候,你好像別無選擇了。
不過也不用害怕,今天我們的現(xiàn)代瀏覽器深度游會非常輕松、快樂。這首先必須感謝一位名叫Mariko Kosaka(小坂真子,https://kosamari.com/)的同行。她在Scripto工作,2018年9月在Google開發(fā)者網(wǎng)站上發(fā)表了“Inside look at modern web browser”系列文章。本文就是她那4篇文章的“集合版”。為什么搞這個“集合版”?因為她的4篇文章寫得實在太好,更難得的是人家親手繪制了一大堆生動的配圖和動畫,這讓深入理解現(xiàn)代瀏覽器變得更加輕松愉快。
好了,言歸正傳。本文分4個部分,對應上述4篇文章(原文鏈接附后)。
架構:以Chrome為例,介紹現(xiàn)代瀏覽器的實現(xiàn)架構。
導航:從輸入URL到獲到HTML響應稱為導航。
渲染:瀏覽器解析HTML、下載外部資源、計算樣式并把網(wǎng)頁繪制到屏幕上。
交互:用戶輸入事件的處理與優(yōu)化。
先來個小小的序言。很多人在開發(fā)網(wǎng)站時,只關注怎么寫自己的代碼,關注怎么提升自己的開發(fā)效率。這些當然重要,但是寫到一定的階段,就應該停下來想想:瀏覽器到底會怎么運行你寫的代碼。如果你能多了解一些瀏覽器,然后對它好一點,那么就會更容易達成你提升用戶體驗的目標。
架構
Web瀏覽器的架構,可以實現(xiàn)為一個進程包含多個線程,也可以實現(xiàn)為很多進程包含少數(shù)線程通過IPC通信。如何實現(xiàn)瀏覽器,并沒有統(tǒng)一的標準。Chrome最新的架構:最上層是瀏覽器進程,負責協(xié)調(diào)承擔各項工作的其他進程,比如實用程序進程、渲染器進程、GPU進程、插件進程等,如下圖所示。

渲染器進程對應新開的標簽頁,每新開一個標簽頁,就會創(chuàng)建一個新的渲染器進程。不僅如此,Chrome還會盡量給每個站點新開一個渲染器進程,包括iframe中的站點,以實現(xiàn)站點隔離。
下面詳細了解一下每個進程的作用,可以參考下圖。
瀏覽器進程:控制瀏覽器這個應用的chrome(主框架)部分,包括地址欄、書簽、前進/后退按鈕等,同時也會處理瀏覽器不可見的高權限任務,如發(fā)送網(wǎng)絡請求、訪問文件。
渲染器進程:負責在標簽頁中顯示網(wǎng)站及處理事件。
插件進程:控制網(wǎng)站用到的所有插件。
GPU進程:在獨立的進程中處理GPU任務。之所以放到獨立的進程,是因為GPU要處理來自多個應用的請求,但要在同一個界面上繪制圖形。

當然,還有其他進程,比如擴展進程、實用程序進程。要知道你的Chrome當前打開了多少個進程,點擊右上角的按鈕,選擇“更多工具”,再選擇“任務管理器”。
Chrome的多進程架構有哪些優(yōu)點呢?
最簡單的情況下,可以想像一個標簽頁就是一個渲染器進程,比如3個標簽頁就是3個渲染器進程。這時候,如果有一個渲染器崩潰了,只要把它關掉即可,不會影響其他標簽頁。如果所有標簽頁都運行在一個進程中,那只要有一個標簽頁卡住,所有標簽頁都會卡住。
除此之外,多進程架構還有助于安全和隔離。因為操作系統(tǒng)有限制進程特權的機制,瀏覽器可以借此限制某些進程的能力。比如,Chrome會限制處理任意用戶輸入的渲染器進程,不讓它任意訪問文件。
由于進程都有自己私有的內(nèi)存空間,因此每個進程可能都會保存某個公共基礎設施(比如Chrome的JavaScript引擎V8)的多個副本。這會導致內(nèi)存占用增多。為節(jié)省內(nèi)存,Chrome會限制自己可以打開的進程數(shù)量。限制的條件取決于設備內(nèi)存和CPU配置。達到限制條件后,Chrome會用一個進程處理同一個站點的多個標簽頁。
Chrome架構進化的目標是將整個瀏覽器程序的不同部分服務化,便于分割或合并。基本思路是在高配設備中,每個服務獨立開進程,保證穩(wěn)定;在低配設備中,多個服務合并為一個進程,節(jié)約資源。同樣的思路也應用到了Android上。
重點說一說站點隔離(http://t.cn/RgNAwLC)。站點隔離是新近引入Chrome的一個里程碑式特性,即每個跨站點iframe都運行一個獨立的渲染器進程。即便像前面說的那樣,每個標簽頁單開一個渲染器進程,但允許跨站點的iframe運行在同一個渲染器進程中并共享內(nèi)存空間,那安全攻擊仍然有可能繞開同源策略(http://t.cn/8s1ySzx),而且有人發(fā)現(xiàn)在現(xiàn)代CPU中,進程有可能讀取任意內(nèi)存(http://t.cn/R8FwHoX)。
進程隔離是隔離站點、確保上網(wǎng)安全最有效的方式。Chrome 67桌面版默認采用站點隔離。站點隔離是多年工程化努力的結果,它并非多開幾個渲染器進程那么簡單。比如,不同的iframe運行在不同進程中,開發(fā)工具在后臺仍然要做到無縫切換,而且即便簡單地Ctrl+F查找也會涉及在不同進程中搜索。
導航
導航涉及瀏覽器進程與線程間為顯示網(wǎng)頁而通信。一切從用戶在瀏覽器中輸入一個URL開始。輸入URL之后,瀏覽器會通過互聯(lián)網(wǎng)獲取數(shù)據(jù)并顯示網(wǎng)頁。從請求網(wǎng)頁到瀏覽器準備渲染網(wǎng)頁的過程,叫做導航。
如前所述,標簽頁外面的一切都由瀏覽器進程處理。瀏覽器進程中有線程(UI線程)負責繪制瀏覽器的按鈕和地址欄,有線程(網(wǎng)絡線程)負責處理網(wǎng)絡請求并從互聯(lián)網(wǎng)接收數(shù)據(jù),有線程(存儲線程)負責訪問文件和存儲數(shù)據(jù)。

下面我們逐步看一看導航的幾個步驟。
第一步:處理輸入。UI線程會判斷用戶輸入的是查詢字符串還是URL。因為Chrome的地址欄同時也是搜索框。

第二步:開始導航。如果輸入的是URL,UI線程會通知網(wǎng)絡線程發(fā)起網(wǎng)絡調(diào)用,獲取網(wǎng)站內(nèi)容。此時標簽頁左端顯示旋轉(zhuǎn)圖標,網(wǎng)絡線程進行DNS查詢、建立TLS連接(對于HTTPS)。網(wǎng)絡線程可能收到服務器的重定向頭部,如HTTP 301。此時網(wǎng)絡線程會跟UI線程溝通,告訴它服務器要求重定向。然后,再發(fā)起對另一個URL的請求。

第三步:讀取響應。服務器返回的響應體到來之后,網(wǎng)絡線程會檢查接收到的前幾個字節(jié)。響應的Content-Type頭部應該包含數(shù)據(jù)類型,如果沒有這個字段,則需要MIME類型嗅探(http://t.cn/Rt2gG2J)??纯碈hrome源碼(http://t.cn/Ai9cZI7D)中的注釋就知道這一塊有多難搞。

如果響應是HTML文件,那下一步就是把數(shù)據(jù)交給渲染器進程。但如果是一個zip文件或其他文件,那就意味著是一個下載請求,需要把數(shù)據(jù)傳給下載管理器。
此時也是“安全瀏覽”(https://safebrowsing.google.com/)檢查的環(huán)節(jié)。如果域名和響應數(shù)據(jù)匹配已知的惡意網(wǎng)站,網(wǎng)絡線程會顯示警告頁。此外,CORB(Cross Origin Read Blocking,https://www.chromium.org/Home/chromium-security/corb-for-developers)檢查也會執(zhí)行,以確保敏感的跨站點數(shù)據(jù)不會發(fā)送給渲染器進程。
第四步:聯(lián)系渲染器進程。所有查檢完畢,網(wǎng)絡線程確認瀏覽器可以導航到用戶請求的網(wǎng)站,于是會通知UI線程數(shù)據(jù)已經(jīng)準備好了。UI線程會聯(lián)系渲染器進程渲染網(wǎng)頁。

由于網(wǎng)絡請求可能要花幾百毫秒才能拿到響應,這里還會應用一個優(yōu)化策略。第二步UI線程要求網(wǎng)絡線程發(fā)送請求后,已經(jīng)知道可能要導航到哪個網(wǎng)站去了。因此在發(fā)送網(wǎng)絡請求的同時,UI線程會提前聯(lián)系或并行啟動一個渲染器進程。這樣在網(wǎng)絡線程收到數(shù)據(jù)后,就已經(jīng)有渲染器進程原地待命了。如果發(fā)生了重定向,這個待命進程可能用不上,而是換作其他進程去處理。
第五步:提交導航。數(shù)據(jù)和渲染器進程都有了,就可以通過IPC從瀏覽器進程向渲染器進程提交導航。渲染器進程也會同時接收到不間斷的HTML數(shù)據(jù)流。當瀏覽器進程收到渲染器進程的確認消息后,導航完成,文檔加載階段開始。

此時,地址欄會更新,安全指示圖標和網(wǎng)站設置UI也會反映新頁面的信息。當前標簽頁面的會話歷史會更新,后退/前進按鈕起作用。為便于標簽頁/會話在關閉標簽頁或窗口后恢復,會話歷史會寫入磁盤。
最后一步:初始加載完成。提交導航之后,渲染器進程將負責加載資源和渲染頁面(具體細節(jié)后面介紹)。而在“完成”渲染后(在所有iframe中的onload事件觸發(fā)且執(zhí)行完成后),渲染器進程會通過IPC給瀏覽器進程發(fā)送一個消息。此時,UI線程停止標簽頁上的旋轉(zhuǎn)圖標。
初始加載完成后,客戶端JavaScript仍然可能加載額外資源并重新渲染頁面。
如果此時用戶在地址又輸入了其他URL呢?瀏覽器進程還會重復上述步驟,導航到新站點。不過在此之前,需要確認已渲染的網(wǎng)站是否關注beforeunload事件。因為標簽頁中的一切,包括JavaScript代碼都由渲染器進程處理,所以瀏覽器進程必須與當前的渲染器進程確認后再導航到新站點。

如果導航請求來自當前渲染器進程(用戶點擊了鏈接或JavaScript運行了window.location = "https://newsite.com"),渲染器進程首先會檢查beforeunload處理程序。然后,它會走一遍與瀏覽器進程觸發(fā)導航同樣的過程。唯一的區(qū)別在于導航請求是由渲染器進程提交給瀏覽器進程的。
導航到不同的網(wǎng)站時,會有一個新的獨立渲染器進程負責處理新導航,而老的渲染器進程要負責處理unload之類的事件。更多細節(jié),可以參考“頁面生命周期API”:http://t.cn/Rey7RIE。

另外,導航階段還可能涉及Service Worker,即網(wǎng)頁應用中的網(wǎng)絡代理服務(http://t.cn/R3SH3HL),開發(fā)者可以通過它控制什么緩存在本地,何時從網(wǎng)絡獲取新數(shù)據(jù)。Service Worker說到底也是需要渲染器進程運行的JavaScript代碼。如果網(wǎng)站注冊了Server Worker,那么導航請求到來時,網(wǎng)絡線程會根據(jù)URL將其匹配出來,此時UI線程就會聯(lián)系一個渲染器進程來執(zhí)行Service Worker的代碼:可能只要從本地緩存讀取數(shù)據(jù),也可能需要發(fā)送網(wǎng)絡請求。

如果Service Worker最終決定從網(wǎng)絡請求數(shù)據(jù),瀏覽器進程與渲染器進程間的這種往返通信會導致延遲。因此,這里會有一個“導航預加載”的優(yōu)化(http://t.cn/Ai9qGJ66),即在Service Worker啟動同時預先加載資源,加載請求通過HTTP頭部與服務器溝通,服務器決定是否完全更新內(nèi)容。

渲染
渲染是渲染器進程內(nèi)部的工作,涉及Web性能的諸多方面(詳細內(nèi)容可以參考這里http://t.cn/Ai9c4nUu)。標簽頁中的一切都由渲染器進程負責處理,其中主線程負責運行大多數(shù)客戶端JavaScript代碼,少量代碼可能會由工作線程處理(如果用到了Web Worker或Service Worker)。合成器(compositor)線程和柵格化(raster)線程負責高效、平滑地渲染頁面。

渲染器進程的核心任務是把HTML、CSS和JavaScript轉(zhuǎn)換成用戶可以交互的網(wǎng)頁接下來,我們從整體上過一遍渲染器進程處理Web內(nèi)容的各個階段。
解析HTML
構建DOM。渲染器進程收到導航的提交消息后,開始接收HTML,其主線程開始解析文本字符串(HTML),并將它轉(zhuǎn)換為DOM(Document Object Model,文檔對象模型)。
DOM是瀏覽器內(nèi)部對頁面的表示,也是JavaScript與之交互的數(shù)據(jù)結構和API。
如何將HTML解析為DOM由HTML標準(http://t.cn/R2NREUt)定義。HTML標準要求瀏覽器兼容錯誤的HTML寫法,因此瀏覽器會“忍氣吞聲”,絕不報錯。詳情可以看看“解析器錯誤處理及怪異情形簡介”(http://t.cn/Ai9c8i5D)。
加載子資源。網(wǎng)站都會用到圖片、CSS和JavaScript等外部資源。瀏覽器需要從緩存或網(wǎng)絡加載這些文件。主線程可以在解析并構建DOM的過程中發(fā)現(xiàn)一個加載一個,但這樣效率太低。為此,Chrome會在解析同時并發(fā)運行“預加載掃描器”,當發(fā)現(xiàn)HTML文檔中有或時,預加載掃描器會將請求提交給瀏覽器進程中的網(wǎng)絡線程。

JavaScript可能阻塞解析。如果HTML解析器碰到標簽,會暫停解析HTML文檔并加載、解析和執(zhí)行JavaScript代碼。因為JavaScript有可能通過document.write()修改文檔,進而改變DOM結構(HTML標準的“解析模型”有一張圖可以一目了然:http://t.cn/Ai9cupLc)。所以HTML解析器必須停下來執(zhí)行JavaScript,然后再恢復解析HTML。至于執(zhí)行JavaScript的細節(jié),大家可以關注V8團隊相關的分享:http://t.cn/RB9qP51。
提示瀏覽器你要加載資源
計算樣式
光有DOM還不行,因為并不知道頁面應該長啥樣。所以接下來,主線程要解析CSS并計算每個DOM節(jié)點的樣式。這個過程就是根據(jù)CSS選擇符,確定每個元素要應用什么樣式。在Chrome開發(fā)工具“計算的樣式”(computed)中可以看每個元素計算后的樣式。

就算網(wǎng)頁沒有提供任何CSS,每個DOM節(jié)點仍然會有計算的樣式。這是因為瀏覽器有一個默認的樣式表,Chrome默認的樣式在這里:http://t.cn/Ai9VALCy。
布局
到這一步,渲染器進程知道了文檔的結構,也知道了每個節(jié)點的樣式。但基于這些信息仍然不足以渲染頁面。比如,你通過電話跟朋友說:“畫一個紅色的大圓形,還有一個藍色的小方形”,你的朋友仍然不知道該畫成什么樣。

布局,就是要找到元素間的幾何位置關系。主線程會遍歷DOM元素及其計算樣式,然后構造一棵布局樹,這棵樹的每個節(jié)點將帶有坐標和大小信息。布局樹與DOM樹的結構類似,但只包含頁面中可見元素的信息。如果元素被應用了display: none,則布局樹中不會包含它(visibility: hidden的元素會包含在內(nèi))。類似地,通過偽類p::before{content: 'Hi!'}添加的內(nèi)容會包含在布局樹中,但DOM樹中卻沒有。

確定頁面的布局要考慮很多很多因素,并不簡單。比如,字體大小、文本換行都會影響段落的形狀,進而影響后續(xù)段落的布局。CSS可讓元素浮動到一邊、隱藏溢出邊界的內(nèi)容、改變文本顯示方向。可想而知,布局階段的任務是非常艱巨的。Chrome有一個工程師團隊專司布局,感興趣的話,可以看看他們這個分享:http://t.cn/Ai9VcjFn(在YouTube上)。
繪制
有了DOM、樣式和布局,仍然不足以渲染頁面。還要解決先畫什么后畫什么,即繪制順序的問題。比如,z-index影響元素疊放,如果有這個屬性,那簡單地按元素在HTML中出現(xiàn)的順序繪制就會出錯。

因此,在這一步,主線程會遍歷布局樹并創(chuàng)建繪制記錄。繪制記錄是對繪制過程的注解,比如“先畫背景,然后畫文本,最后畫矩形”。如果你用過,應該更容易理解這一點。

渲染是一個流水線作業(yè)(pipeline):前一道工序的輸出就是下一道工序的輸入。這意味著如果布局樹有變化,則相應的繪制記錄也要重新生成。

如果元素有動畫,瀏覽器就需要每幀運行一次渲染流水線。目前顯示器的刷新率為每秒60次(60fps),也就是說每秒更新60幀,動畫會顯得很流暢。如果中間缺了幀,那頁面看起來就會“閃眼睛”。

即便渲染操作的頻率能跟上屏幕刷新率,但由于計算發(fā)生在主線程上,而主線程可能因為運行JavaScript被阻塞。此時動畫會因為阻塞被卡住。

此時,可以使用requestAnimationFrame()將涉及動畫的JavaScript操作分塊并調(diào)度到每一幀的開始去運行。對于耗時的不必操作DOM的JavaScript操作,可以考慮Web Worker(http://t.cn/Ai9VBqs9),避免阻塞主線程。
合成
知道了文檔結構、每個元素的樣式、頁面的幾何關系,以及繪制順序,接下來就該繪制頁面了。具體該怎么繪制呢?把上述信息轉(zhuǎn)換為屏幕上的像素叫做柵格化。
最簡單的方式,可能就是把頁面在當前視口中的部分先轉(zhuǎn)換為像素。然后隨著用戶滾動頁面,再移動柵格化的畫框(frame),填補缺失的部分。Chrome最早的版本就是這樣干的。

但現(xiàn)代瀏覽器會使用一個更高級的步驟叫合成。什么是合成?合成(composite)是將頁面不同部分先分層并分別柵格化,然后再通過獨立的合成器線程合成頁面。這樣當用戶滾動頁面時,因為層都已經(jīng)柵格化,所以瀏覽器唯一要做的就是合成一個新的幀。而動畫也可以用同樣的方式實現(xiàn):先移動層,再合成幀。

怎么分層?為了確定哪個元素應該在哪一層,主線程會遍歷布局樹并創(chuàng)建分層樹(這一部分在開發(fā)工具的“性能”面板中叫“Update Layer Tree”)。如果頁面某些部分應該獨立一層(如滑入的菜單)但卻沒有,那你可以在CSS中給它加上will-change屬性(http://t.cn/R7IJCx2)來提醒瀏覽器。

分層并不是越多越好,合成過多的層有可能還不如每幀都對頁面中的一小部分執(zhí)行一次柵格化更快。關于這里邊的權衡,可以參考:http://t.cn/Ai9fiJiM。
創(chuàng)建了分層樹,確定了繪制順序,主線程就會把這些信息提交給合成器線程。合成器線程接下來負責將每一層轉(zhuǎn)換為像素——柵格化。一層有可能跟頁面一樣大,此時合成器線程會將它切成小片(tile),再把每一片發(fā)給柵格化線程。柵格化線程將每一小片轉(zhuǎn)換為像素后將它們保存在GPU的內(nèi)存中。

合成器線程會安排柵格化線程優(yōu)先轉(zhuǎn)換視口(及附近)的小片。而構成一層的小片也會轉(zhuǎn)換為不同分辨率的版本,以便在用戶縮放時使用。
所有小片都柵格化以后,合成器線程會收集叫做“繪制方塊”(draw quad)的小片信息,以創(chuàng)建合成器幀。
繪制方塊:包含小片的內(nèi)存地址、頁面位置等合成頁面相關的信息
合成器幀:由從多繪制方塊拼成的頁面中的一幀
創(chuàng)建好的合成器幀會通過IPC提交給瀏覽器進程。與此同時,為更新瀏覽器界面,UI線程可能還會添加另一個合成器幀;或者因為有擴展,其他渲染器進程也可能添加額外的合成器幀。所有這些合成器幀都會發(fā)送給GPU,以便最終顯示在屏幕上。如果發(fā)生滾動事件,合成器線程會再創(chuàng)建新的合成器幀并發(fā)送給GPU。

使用合成的好處是不用牽扯主線程。合成器線程不用等待樣式計算或JavaScript執(zhí)行。這也是為什么“只需合成的動畫”(http://t.cn/Ai9fO8OW)被認為性能最佳的原因。因為如果布局和繪制需要再次計算,那還得用到主線程。
交互
最后,我們看一看合成器如何處理用戶交互。說到用戶交互,有人可能只會想到在文本框里打字或點擊鼠標。實際上,從瀏覽器的角度看,交互意味著來自用戶的任何輸入:鼠標滾輪轉(zhuǎn)動、觸摸屏幕、鼠標懸停,這些都是交互。
當用戶交互比如觸摸事件發(fā)生時,瀏覽器進程首先接收到該手勢。但是,瀏覽器進程僅僅知道手勢發(fā)生在哪里,因為標簽頁中的內(nèi)容是渲染器進程處理。因此瀏覽器進程會把事件類型(如touchstart)及其坐標發(fā)送給渲染器進程。渲染器進程會處理這個事件,即根據(jù)事件目標來運行注冊的監(jiān)聽程序。

具體來說,輸入事件是由渲染器進程中的合成器線程處理的。如前所述,如果頁面上沒有注冊事件監(jiān)聽程序,那合成器線程可以完全獨立于主線程生成新的合成器幀。但是如果頁面上注冊了事件監(jiān)聽程序呢?此時合成器線程怎么知道是否有事件要處理?
這就涉及一個概念,叫“非快速滾動區(qū)”(non-fast scrollable region)。我們知道,運行JavaScript是主線程的活兒。在頁面合成后,合成器線程會給附加了事件處理程序的頁面區(qū)域打上“Non-Fast Scrollable Region”的記號。有了這個記號,合成器線程就可以在該區(qū)域發(fā)生事件時把事件發(fā)送給主線程。

如果事件發(fā)生在這個區(qū)域外,那合成器線程會繼續(xù)合成新幀而不會等待主線程。
提到注冊事件,有一個常見的問題要注意。很多人喜歡使用事件委托來注冊處理程序。這是利用事件冒泡原理,把事件注冊到最外層元素上,然后再根據(jù)事件目標決定是否執(zhí)行任務。
document.body.addEventListener('touchstart',?evt?=>?{
????if?(evt.target?===?area)?{
????????evt.preventDefault()
????}
})
一個事件處理程序就可以面向多個元素,這種高效的寫法因此很流行。然而,從瀏覽器的角度來看,這樣會導致整個頁面被標記為“非快速滾動區(qū)”。這也就意味著,即便事件發(fā)生在那些不需要處理的元素上,合成器線程也要每次都跟主線程溝通,并等待它的回應。于是,合成器線程平滑滾動的優(yōu)點就被抵銷了。

為緩沖使用事件委托帶來的副作用,可以在注冊事件時傳入passive: true。這個選項會提醒瀏覽器,你仍然希望主線程處理事件,但與此同時合成器線程也可以繼續(xù)合成新的幀。
document.body.addEventListener('touchstart',?evt?=>?{
??...
},?{?passive:?true?})
此外,檢查事件是否可以取消也是一個優(yōu)化策略。假設頁面中有一個盒子,你想限制盒子中的內(nèi)容只能水平滾動。

使用passive: true可以讓頁面平滑滾動,但為了限制滾動方向而調(diào)用prevenDefault則不會避免垂直滾動。此時可以檢查evt.cancelable。
document.body.addEventListener('pointermove',?evt?=>?{
????if?(evt.cancelable)?{
????????evt.preventDefault();?// 阻止原生滾動
????????/*
????????* 其他操作
????????*/
????}
},?{?passive:?true?});
當然,也可以使用CSS規(guī)則如touch-action完全避免使用事件處理程序。
#area?{
????touch-action:?pan-x;
}
合成器線程把事件發(fā)送給主線程以后,要做的第一件事就是通過命中測試(hit test)找到事件目標。命中測試就是根據(jù)渲染進程生成的繪制記錄數(shù)據(jù)和事件坐標找到下方的元素。

另外,事件還有一個觸發(fā)頻率的問題。通常的觸屏設備每秒會產(chǎn)生60~120次觸碰事件,而鼠標每秒會產(chǎn)生約100次事件。換句話說,輸入事件具有比每秒刷新60次的屏幕更高的保真度。
如果像touchmove這種連續(xù)事件,以每秒120次的頻率發(fā)送到主線程,相比更慢的屏幕刷新率而言,就會導致過多的命中測試和JavaScript執(zhí)行。

為把對主線程過多的調(diào)用降至最少,Chrome會合并(coalesce)連續(xù)觸發(fā)的事件(如wheel、mousewheel、mousemove、pointermove、touchmove),并將它們延遲到恰好在下一次requestAnimationFrame之前派發(fā)。

對于其他離散觸發(fā)的事件,像keydown、keyup、mouseup、mousedown、touchstart和touchend會立即派發(fā)。
合并后的事件在多數(shù)情況下足以保證不錯的用戶體驗。但是,在一些特殊應用場景下,比如需要基于touchmove事件的坐標生成軌跡的繪圖應用,合并事件就會導致丟失一些坐標,影響所繪線條的平滑度。

此時,可以使用指針事件的getCoalescedEvents方法,取得被合并事件的信息:
window.addEventListener('pointermove',?event?=>?{
????const?events?=?event.getCoalescedEvents();
????for?(let?event?of?events)?{
????????const?x?=?event.pageX;
????????const?y?=?event.pageY;
????????// 使用x和y坐標畫線
????}
});
這是個小小的結尾。相信不少前端開發(fā)者早已知道給標簽添加defer、async屬性的作用。通過閱讀本文,你應該也知道了為什么在注冊事件監(jiān)聽器時最好傳入passive: true選項,知道了CSS的will-change屬性會讓瀏覽器做出不同的決策。事實上,不止上面這些,看完看懂這篇文章,你甚至也會對其他關于瀏覽器性能優(yōu)化的細節(jié)感到豁然開朗,從而對更多關于網(wǎng)頁性能的話題會產(chǎn)生興起。而這正是深入理解現(xiàn)代瀏覽器的重要意義和價值所在,因為它為我們打開了一扇大門。
原文鏈接:
https://developers.google.com/web/updates/2018/09/inside-browser-part1
https://developers.google.com/web/updates/2018/09/inside-browser-part2
https://developers.google.com/web/updates/2018/09/inside-browser-part3
https://developers.google.com/web/updates/2018/09/inside-browser-part4
再次感謝原文作者:Mariko Kosaka
她的網(wǎng)站:https://kosamari.com/
她的Twitter:https://twitter.com/kosamari

