整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          你不知道的瀏覽器渲染原理

          瀏覽器的內核是指支持瀏覽器運行的最核心的程序,分為兩個部分的,一是渲染引擎,另一個是 JS 引擎。渲染引擎在不同的瀏覽器中也不是都相同的。目前市面上常見的瀏覽器內核可以分為這四種:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。這里面大家最耳熟能詳的可能就是 Webkit 內核了,Webkit 內核是當下瀏覽器世界真正的霸主。

          本文我們就以 Webkit 為例,對現代瀏覽器的渲染過程進行一個深度的剖析。

          想閱讀更多優質文章請猛戳GitHub 博客。

          頁面加載過程

          在介紹瀏覽器渲染過程之前,我們簡明扼要介紹下頁面的加載過程,有助于更好理解后續渲染過程。

          要點如下:

          • 瀏覽器根據 DNS 服務器得到域名的 IP 地址;
          • 向這個 IP 的機器發送 HTTP 請求;
          • 服務器收到、處理并返回 HTTP 請求;
          • 瀏覽器得到返回內容。

          例如在瀏覽器輸入https://juejin.im/timeline,然后經過 DNS 解析,juejin.im對應的 IP 是36.248.217.149(不同時間、地點對應的 IP 可能會不同)。然后瀏覽器向該 IP 發送 HTTP 請求。

          服務端接收到 HTTP 請求,然后經過計算(向不同的用戶推送不同的內容),返回 HTTP 請求,返回的內容如下:


          其實就是一堆 HMTL 格式的字符串,因為只有 HTML 格式瀏覽器才能正確解析,這是 W3C 標準的要求。接下來就是瀏覽器的渲染過程。

          瀏覽器渲染過程


          瀏覽器渲染過程大體分為如下三部分:

          1)瀏覽器會解析三個東西:

          一是 HTML/SVG/XHTML,HTML 字符串描述了一個頁面的結構,瀏覽器會把 HTML 結構字符串解析轉換 DOM 樹形結構。


          二是 CSS,解析 CSS 會產生 CSS 規則樹,它和 DOM 結構比較像。


          三是 Javascript 腳本,等到 Javascript 腳本文件加載后, 通過 DOM API 和 CSSOM API 來操作 DOM Tree 和 CSS Rule Tree。


          2)解析完成后,瀏覽器引擎會通過 DOM Tree 和 CSS Rule Tree 來構造 Rendering Tree。

          • Rendering Tree 渲染樹并不等同于 DOM 樹,渲染樹只會包括需要顯示的節點和這些節點的樣式信息。
          • CSS 的 Rule Tree 主要是為了完成匹配并把 CSS Rule 附加上 Rendering Tree 上的每個 Element(也就是每個 Frame)。
          • 然后,計算每個 Frame 的位置,這又叫 layout 和 reflow 過程。

          3)最后通過調用操作系統 Native GUI 的 API 繪制。

          接下來我們針對這其中所經歷的重要步驟詳細闡述

          構建 DOM

          瀏覽器會遵守一套步驟將 HTML 文件轉換為 DOM 樹。宏觀上,可以分為幾個步驟:


          瀏覽器從磁盤或網絡讀取 HTML 的原始字節,并根據文件的指定編碼(例如 UTF-8)將它們轉換成字符串。

          在網絡中傳輸的內容其實都是 0 和 1 這些字節數據。當瀏覽器接收到這些字節數據以后,它會將這些字節數據轉換為字符串,也就是我們寫的代碼。

          將字符串轉換成 Token,例如:<html>、<body>等。Token 中會標識出當前 Token 是“開始標簽”或是“結束標簽”亦或是“文本”等信息

          這時候你一定會有疑問,節點與節點之間的關系如何維護?

          事實上,這就是 Token 要標識“起始標簽”和“結束標簽”等標識的作用。例如“title”Token 的起始標簽和結束標簽之間的節點肯定是屬于“head”的子節點。


          上圖給出了節點之間的關系,例如:“Hello”Token 位于“title”開始標簽與“title”結束標簽之間,表明“Hello”Token 是“title”Token 的子節點。同理“title”Token 是“head”Token 的子節點。

          • 生成節點對象并構建 DOM

          事實上,構建 DOM 的過程中,不是等所有 Token 都轉換完成后再去生成節點對象,而是一邊生成 Token 一邊消耗 Token 來生成節點對象。換句話說,每個 Token 被生成后,會立刻消耗這個 Token 創建出節點對象。注意:帶有結束標簽標識的 Token 不會創建節點對象。

          接下來我們舉個例子,假設有段 HTML 文本:

          復制代碼

          <html>
          <head>
           <title>Web page parsing</title>
          </head>
          <body>
           <div>
           <h1>Web page parsing</h1>
           <p>This is an example Web page.</p>
           </div>
          </body>
          </html>
          

          上面這段 HTML 會解析成這樣:


          構建 CSSOM

          DOM 會捕獲頁面的內容,但瀏覽器還需要知道頁面如何展示,所以需要構建 CSSOM。

          構建 CSSOM 的過程與構建 DOM 的過程非常相似,當瀏覽器接收到一段 CSS,瀏覽器首先要做的是識別出 Token,然后構建節點并生成 CSSOM。


          在這一過程中,瀏覽器會確定下每一個節點的樣式到底是什么,并且這一過程其實是很消耗資源的。因為樣式你可以自行設置給某個節點,也可以通過繼承獲得。在這一過程中,瀏覽器得遞歸 CSSOM 樹,然后確定具體的元素到底是什么樣式。

          注意:CSS 匹配 HTML 元素是一個相當復雜和有性能問題的事情。所以,DOM 樹要小,CSS 盡量用 id 和 class,千萬不要過渡層疊下去

          構建渲染樹

          當我們生成 DOM 樹和 CSSOM 樹以后,就需要將這兩棵樹組合為渲染樹。


          在這一過程中,不是簡單的將兩者合并就行了。渲染樹只會包括需要顯示的節點和這些節點的樣式信息,如果某個節點是 display: none 的,那么就不會在渲染樹中顯示。

          我們或許有個疑惑:瀏覽器如果渲染過程中遇到 JS 文件怎么處理

          渲染過程中,如果遇到<script>就停止渲染,執行 JS 代碼。因為瀏覽器有 GUI 渲染線程與 JS 引擎線程,為了防止渲染出現不可預期的結果,這兩個線程是互斥的關系。JavaScript 的加載、解析與執行會阻塞 DOM 的構建,也就是說,在構建 DOM 時,HTML 解析器若遇到了 JavaScript,那么它會暫停構建 DOM,將控制權移交給 JavaScript 引擎,等 JavaScript 引擎運行完畢,瀏覽器再從中斷的地方恢復 DOM 構建。

          也就是說,如果你想首屏渲染的越快,就越不應該在首屏就加載 JS 文件,這也是都建議將 script 標簽放在 body 標簽底部的原因。當然在當下,并不是說 script 標簽必須放在底部,因為你可以給 script 標簽添加 defer 或者 async 屬性(下文會介紹這兩者的區別)。

          JS 文件不只是阻塞 DOM 的構建,它會導致 CSSOM 也阻塞 DOM 的構建

          原本 DOM 和 CSSOM 的構建是互不影響,井水不犯河水,但是一旦引入了 JavaScript,CSSOM 也開始阻塞 DOM 的構建,只有 CSSOM 構建完畢后,DOM 再恢復 DOM 構建。

          這是什么情況?

          這是因為 JavaScript 不只是可以改 DOM,它還可以更改樣式,也就是它可以更改 CSSOM。因為不完整的 CSSOM 是無法使用的,如果 JavaScript 想訪問 CSSOM 并更改它,那么在執行 JavaScript 時,必須要能拿到完整的 CSSOM。所以就導致了一個現象,如果瀏覽器尚未完成 CSSOM 的下載和構建,而我們卻想在此時運行腳本,那么瀏覽器將延遲腳本執行和 DOM 構建,直至其完成 CSSOM 的下載和構建。也就是說,在這種情況下,瀏覽器會先下載和構建 CSSOM,然后再執行 JavaScript,最后在繼續構建 DOM


          布局與繪制

          當瀏覽器生成渲染樹以后,就會根據渲染樹來進行布局(也可以叫做回流)。這一階段瀏覽器要做的事情是要弄清楚各個節點在頁面中的確切位置和大小。通常這一行為也被稱為“自動重排”。

          布局流程的輸出是一個“盒模型”,它會精確地捕獲每個元素在視口內的確切位置和尺寸,所有相對測量值都將轉換為屏幕上的絕對像素。

          布局完成后,瀏覽器會立即發出“Paint Setup”和“Paint”事件,將渲染樹轉換成屏幕上的像素。

          以上我們詳細介紹了瀏覽器工作流程中的重要步驟,接下來我們討論幾個相關的問題:

          幾點補充說明

          1.async 和 defer 的作用是什么?有什么區別?

          接下來我們對比下 defer 和 async 屬性的區別:


          其中藍色線代表 JavaScript 加載;紅色線代表 JavaScript 執行;綠色線代表 HTML 解析。

          1)情況 1<script src="script.js"></script>

          沒有 defer 或 async,瀏覽器會立即加載并執行指定的腳本,也就是說不等待后續載入的文檔元素,讀到就加載并執行。

          2)情況 2<script async src="script.js"></script> (異步下載)

          async 屬性表示異步執行引入的 JavaScript,與 defer 的區別在于,如果已經加載好,就會開始執行——無論此刻是 HTML 解析階段還是 DOMContentLoaded 觸發之后。需要注意的是,這種方式加載的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發之前或之后執行,但一定在 load 觸發之前執行。

          3)情況 3 <script defer src="script.js"></script>(延遲執行)

          defer 屬性表示延遲執行引入的 JavaScript,即這段 JavaScript 加載時 HTML 并未停止解析,這兩個過程是并行的。整個 document 解析完畢且 defer-script 也加載完成之后(這兩件事情的順序無關),會執行所有由 defer-script 加載的 JavaScript 代碼,然后觸發 DOMContentLoaded 事件。

          defer 與相比普通 script,有兩點區別:載入 JavaScript 文件時不阻塞 HTML 的解析,執行階段被放到 HTML 標簽解析完成之后。

          在加載多個 JS 腳本的時候,async 是無順序的加載,而 defer 是有順序的加載。

          2. 為什么操作 DOM 慢?

          把 DOM 和 JavaScript 各自想象成一個島嶼,它們之間用收費橋梁連接。——《高性能 JavaScript》

          JS 是很快的,在 JS 中修改 DOM 對象也是很快的。在 JS 的世界里,一切是簡單的、迅速的。但 DOM 操作并非 JS 一個人的獨舞,而是兩個模塊之間的協作。

          因為 DOM 是屬于渲染引擎中的東西,而 JS 又是 JS 引擎中的東西。當我們用 JS 去操作 DOM 時,本質上是 JS 引擎和渲染引擎之間進行了“跨界交流”。這個“跨界交流”的實現并不簡單,它依賴了橋接接口作為“橋梁”(如下圖)。


          過“橋”要收費——這個開銷本身就是不可忽略的。我們每操作一次 DOM(不管是為了修改還是僅僅為了訪問其值),都要過一次“橋”。過“橋”的次數一多,就會產生比較明顯的性能問題。因此“減少 DOM 操作”的建議,并非空穴來風。

          3. 你真的了解回流和重繪嗎?

          渲染的流程基本上是這樣(如下圖黃色的四個步驟):

          1. 計算 CSS 樣式

          2. 構建 Render Tree

          3.Layout – 定位坐標和大小

          4. 正式開畫


          注意:上圖流程中有很多連接線,這表示了 Javascript 動態修改了 DOM 屬性或是 CSS 屬性會導致重新 Layout,但有些改變不會重新 Layout,就是上圖中那些指到天上的箭頭,比如修改后的 CSS rule 沒有被匹配到元素。

          這里重要要說兩個概念,一個是 Reflow,另一個是 Repaint

          重繪:當我們對 DOM 的修改導致了樣式的變化、卻并未影響其幾何屬性(比如修改了顏色或背景色)時,瀏覽器不需重新計算元素的幾何屬性、直接為該元素繪制新的樣式(跳過了上圖所示的回流環節)。

          回流:當我們對 DOM 的修改引發了 DOM 幾何尺寸的變化(比如修改元素的寬、高或隱藏元素等)時,瀏覽器需要重新計算元素的幾何屬性(其他元素的幾何屬性和位置也會因此受到影響),然后再將計算的結果繪制出來,這個過程就是回流(也叫重排)。

          我們知道,當網頁生成的時候,至少會渲染一次。在用戶訪問的過程中,還會不斷重新渲染。重新渲染會重復回流 + 重繪或者只有重繪。

          回流必定會發生重繪,重繪不一定會引發回流。重繪和回流會在我們設置節點樣式時頻繁出現,同時也會很大程度上影響性能。回流所需的成本比重繪高的多,改變父節點里的子節點很可能會導致父節點的一系列回流。

          1)常見引起回流屬性和方法

          任何會改變元素幾何信息 (元素的位置和尺寸大小) 的操作,都會觸發回流,

          • 添加或者刪除可見的 DOM 元素;
          • 元素尺寸改變——邊距、填充、邊框、寬度和高度;
          • 內容變化,比如用戶在 input 框中輸入文字;
          • 瀏覽器窗口尺寸改變——resize 事件發生時;
          • 計算 offsetWidth 和 offsetHeight 屬性;
          • 設置 style 屬性的值。

          2)常見引起重繪屬性和方法


          3)如何減少回流、重繪

          • 使用 transform 替代 top;
          • 使用 visibility 替換 display: none ,因為前者只會引起重繪,后者會引發回流(改變了布局);
          • 不要把節點的屬性值放在一個循環里當成循環里的變量。

          復制代碼

          for(let i = 0; i < 1000; i++) {
           // 獲取 offsetTop 會導致回流,因為需要去獲取正確的值
           console.log(document.querySelector('.test').style.offsetTop)
          }
          
          • 不要使用 table 布局,可能很小的一個小改動會造成整個 table 的重新布局;
          • 動畫實現的速度的選擇,動畫速度越快,回流次數越多,也可以選擇使用 requestAnimationFrame;
          • CSS 選擇符從右往左匹配查找,避免節點層級過多;
          • 將頻繁重繪或者回流的節點設置為圖層,圖層能夠阻止該節點的渲染行為影響別的節點。比如對于 video 標簽來說,瀏覽器會自動將該節點變為圖層。

          性能優化策略

          基于上面介紹的瀏覽器渲染原理,DOM 和 CSSOM 結構構建順序,初始化可以對頁面渲染做些優化,提升頁面性能。

          • JS 優化: <script> 標簽加上 defer 屬性 和 async 屬性 用于在不阻塞頁面文檔解析的前提下,控制腳本的下載和執行。
          • defer 屬性: 用于開啟新的線程下載腳本文件,并使腳本在文檔解析完成后執行。
          • async 屬性: HTML5 新增屬性,用于異步下載腳本文件,下載完畢立即解釋執行代碼。
          • CSS 優化: <link> 標簽的 rel 屬性 中的屬性值設置為 preload 能夠讓你在你的 HTML 頁面中可以指明哪些資源是在頁面加載完成后即刻需要的,最優的配置加載順序,提高渲染性能。

          總結

          綜上所述,我們得出這樣的結論:

          • 瀏覽器工作流程:構建 DOM -> 構建 CSSOM -> 構建渲染樹 -> 布局 -> 繪制。
          • CSSOM 會阻塞渲染,只有當 CSSOM 構建完畢后才會進入下一個階段構建渲染樹。
          • 通常情況下 DOM 和 CSSOM 是并行構建的,但是當瀏覽器遇到一個不帶 defer 或 async 屬性的 script 標簽時,DOM 構建將暫停,如果此時又恰巧瀏覽器尚未完成 CSSOM 的下載和構建,由于 JavaScript 可以修改 CSSOM,所以需要等 CSSOM 構建完畢后再執行 JS,最后才重新 DOM 構建。

          參考文章

          • https://segmentfault.com/q/1010000000640869
          • https://coolshell.cn/articles/9666.html
          • https://juejin.im/book/5bdc715fe51d454e755f75ef/section/5c024ecbf265da616a476638
          • https://mp.weixin.qq.com/s?__biz=MzA5NzkwNDk3MQ==&mid=2650588806&idx=1&sn=408a54e7c8102fd6944c9a40b119015a&chksm=8891d6a2bfe65fb42f493fe9a4dab672dd7e440f31e753196cee0cfbc6696e4f8dd3a669e040&mpshare=1&scene=1&srcid=1228ZrXsmbZKcgCSu7zTVDwy#
          • https://juejin.im/book/5b936540f265da0a9624b04b/section/5bac3a4df265da0aa81c043c
          • https://juejin.im/book/5c47343bf265da612b13e5c0/section/5c4737375188255de8397ae3
          • https://juejin.im/book/5a8f9ddcf265da4e9f6fb959/section/5a8f9f7bf265da4e82635e46

          更多內容,請關注前端之巔。

          者 | SHERlocked93

          責編 | 胡巍巍

          大多數設備的刷新頻率是60Hz,也就說是瀏覽器對每一幀畫面的渲染工作要在16ms內完成,超出這個時間,頁面的渲染就會出現卡頓現象,影響用戶體驗。前端的用戶體驗給了前端直觀的印象,因此對B/S架構的開發人員來說,熟悉瀏覽器的內部執行原理顯得尤為重要。

          瀏覽器主要組成與瀏覽器線程

          1.1 瀏覽器組件

          瀏覽器大體上由以下幾個組件組成,各個瀏覽器可能有一點不同。

          • 界面控件 – 包括地址欄,前進后退,書簽菜單等窗口上除了網頁顯示區域以外的部分

          • 瀏覽器引擎 – 查詢與操作渲染引擎的接口

          • 渲染引擎 – 負責顯示請求的內容。比如請求到HTML, 它會負責解析HTML、CSS并將結果顯示到窗口中

          • 網絡 – 用于網絡請求, 如HTTP請求。它包括平臺無關的接口和各平臺獨立的實現

          • UI后端 – 繪制基礎元件,如組合框與窗口。它提供平臺無關的接口,內部使用操作系統的相應實現

          • JS解釋器 - 用于解析執行JavaScript代碼

          • 數據存儲持久層 - 瀏覽器需要把所有數據存到硬盤上,如cookies。新的HTML5規范規定了一個完整(雖然輕量級)的瀏覽器中的數據庫 web database

          注意:chrome瀏覽器與其他瀏覽器不同,chrome使用多個渲染引擎實例,每個Tab頁一個,即每個Tab都是一個獨立進程。

          1.2 瀏覽器中的進程與線程

          Chrome瀏覽器使用多個進程來隔離不同的網頁,在Chrome中打開一個網頁相當于起了一個進程,每個tab網頁都有由其獨立的渲染引擎實例。因為如果非多進程的話,如果瀏覽器中的一個tab網頁崩潰,將會導致其他被打開的網頁應用。另外相對于線程,進程之間是不共享資源和地址空間的,所以不會存在太多的安全問題,而由于多個線程共享著相同的地址空間和資源,所以會存在線程之間有可能會惡意修改或者獲取非授權數據等復雜的安全問題。

          在內核控制下各線程相互配合以保持同步,一個瀏覽器通常由以下常駐線程組成:

          1. GUI 渲染線程

          GUI渲染線程負責渲染瀏覽器界面HTML元素,當界面需要重繪(Repaint)或由于某種操作引發回流(reflow)時,該線程就會執行。在Javascript引擎運行腳本期間,GUI渲染線程都是處于掛起狀態的,也就是說被凍結了.

          2. JavaScript引擎線程

          JS為處理頁面中用戶的交互,以及操作DOM樹、CSS樣式樹來給用戶呈現一份動態而豐富的交互體驗和服務器邏輯的交互處理。如果JS是多線程的方式來操作這些UI DOM,則可能出現UI操作的沖突;如果JS是多線程的話,在多線程的交互下,處于UI中的DOM節點就可能成為一個臨界資源,假設存在兩個線程同時操作一個DOM,一個負責修改一個負責刪除,那么這個時候就需要瀏覽器來裁決如何生效哪個線程的執行結果,當然我們可以通過鎖來解決上面的問題。但為了避免因為引入了鎖而帶來更大的復雜性,JS在最初就選擇了單線程執行。

          GUI渲染線程與JS引擎線程互斥的,是由于JavaScript是可操縱DOM的,如果在修改這些元素屬性同時渲染界面(即JavaScript線程和UI線程同時運行),那么渲染線程前后獲得的元素數據就可能不一致。當JavaScript引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到引擎線程空閑時立即被執行。由于GUI渲染線程與JS執行線程是互斥的關系,當瀏覽器在執行JS程序的時候,GUI渲染線程會被保存在一個隊列中,直到JS程序執行完成,才會接著執行。因此如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞的感覺。

          3. 定時觸發器線程

          瀏覽器定時計數器并不是由JS引擎計數的, 因為JS引擎是單線程的, 如果處于阻塞線程狀態就會影響記計時的準確, 因此通過單獨線程來計時并觸發定時是更為合理的方案。

          4. 事件觸發線程

          當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理。這些事件可以是當前執行的代碼塊如定時任務、也可來自瀏覽器內核的其他線程如鼠標點擊、AJAX異步請求等,但由于JS的單線程關系所有這些事件都得排隊等待JS引擎處理。

          5. 異步http請求線程

          在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求,將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件放到JS引擎的處理隊列中等待處理。

          渲染過程

          2.1 渲染流程

          用戶請求的HTML文本(text/html)通過瀏覽器的網絡層到達渲染引擎后,渲染工作開始。每次通常渲染不會超過8K的數據塊,其中基礎的渲染流程圖:

          webkit引擎渲染的詳細流程,其他引擎渲染流程稍有不同:

          渲染流程有四個主要步驟:

          1. 解析HTML生成DOM樹 - 渲染引擎首先解析HTML文檔,生成DOM樹

          2. 構建Render樹 - 接下來不管是內聯式,外聯式還是嵌入式引入的CSS樣式會被解析生成CSSOM樹,根據DOM樹與CSSOM樹生成另外一棵用于渲染的樹-渲染樹(Render tree),

          3. 布局Render樹 - 然后對渲染樹的每個節點進行布局處理,確定其在屏幕上的顯示位置

          4. 繪制Render樹 - 最后遍歷渲染樹并用UI后端層將每一個節點繪制出來

          以上步驟是一個漸進的過程,為了提高用戶體驗,渲染引擎試圖盡可能快的把結果顯示給最終用戶。它不會等到所有HTML都被解析完才創建并布局渲染樹。它會在從網絡層獲取文檔內容的同時把已經接收到的局部內容先展示出來。

          2.2 渲染細節

          1. 生成DOM樹

          DOM樹的構建過程是一個深度遍歷過程:當前節點的所有子節點都構建好后才會去構建當前節點的下一個兄弟節點。DOM樹的根節點就是document對象。

          DOM樹的生成過程中可能會被CSS和JS的加載執行阻塞,具體可以參見下一章。當HTML文檔解析過程完畢后,瀏覽器繼續進行標記為deferred模式的腳本加載,然后就是整個解析過程的實際結束觸發DOMContentLoaded事件,并在async文檔文檔執行完之后觸發load事件。

          2. 生成Render樹

          生成DOM樹的同時會生成樣式結構體CSSOM(CSS Object Model)Tree,再根據CSSOM和DOM樹構造渲染樹Render Tree,渲染樹包含帶有顏色,尺寸等顯示屬性的矩形,這些矩形的順序與顯示順序基本一致。從MVC的角度來說,可以將Render樹看成是V,DOM樹與CSSOM樹看成是M,C則是具體的調度者,比HTMLDocumentParser等。

          可以這么說,沒有DOM樹就沒有Render樹,但是它們之間不是簡單的一對一的關系。Render樹是用于顯示,那不可見的元素當然不會在這棵樹中出現了,譬如 <head>。除此之外,display等于none的也不會被顯示在這棵樹里頭,但是visibility等于hidden的元素是會顯示在這棵樹里頭的。

          3. DOM樹與Render樹

          DOM對象類型很豐富,什么head、title、div,而Render樹相對來說就比較單一了,畢竟它的職責就是為了以后的顯示渲染用嘛。Render樹的每一個節點我們叫它渲染器renderer。

          一棵Render樹大概是醬紫,左邊是DOM樹,右邊是Render樹:

          從上圖我們可以看出,renderer與DOM元素是相對應的,但并不是一一對應,有些DOM元素沒有對應的renderer,而有些DOM元素卻對應了好幾個renderer,對應多個renderer的情況是普遍存在的,就是為了解決一個renderer描述不清楚如何顯示出來的問題,譬如有下拉列表的select元素,我們就需要三個renderer:一個用于顯示區域,一個用于下拉列表框,還有一個用于按鈕。

          另外,renderer與DOM元素的位置也可能是不一樣的。那些添加了 float或者 position:absolute的元素,因為它們脫離了正常的文檔流,構造Render樹的時候會針對它們實際的位置進行構造。

          4. 布局與繪制

          上面確定了renderer的樣式規則后,然后就是重要的顯示元素布局了。當renderer構造出來并添加到Render樹上之后,它并沒有位置跟大小信息,為它確定這些信息的過程,接下來是布局(layout)。

          瀏覽器進行頁面布局基本過程是以瀏覽器可見區域為畫布,左上角為 (0,0)基礎坐標,從左到右,從上到下從DOM的根節點開始畫,首先確定顯示元素的大小跟位置,此過程是通過瀏覽器計算出來的,用戶CSS中定義的量未必就是瀏覽器實際采用的量。如果顯示元素有子元素得先去確定子元素的顯示信息。

          布局階段輸出的結果稱為box盒模型(width,height,margin,padding,border,left,top,…),盒模型精確表示了每一個元素的位置和大小,并且所有相對度量單位此時都轉化為了絕對單位。

          在繪制(painting)階段,渲染引擎會遍歷Render樹,并調用renderer的 paint 方法,將renderer的內容顯示在屏幕上。繪制工作是使用UI后端組件完成的。

          5. 回流與重繪

          回流(reflow):當瀏覽器發現某個部分發生了點變化影響了布局,需要倒回去重新渲染。reflow 會從 <html>這個 root frame 開始遞歸往下,依次計算所有的結點幾何尺寸和位置。reflow 幾乎是無法避免的。現在界面上流行的一些效果,比如樹狀目錄的折疊、展開(實質上是元素的顯示與隱藏)等,都將引起瀏覽器的 reflow。鼠標滑過、點擊……只要這些行為引起了頁面上某些元素的占位面積、定位方式、邊距等屬性的變化,都會引起它內部、周圍甚至整個頁面的重新渲染。通常我們都無法預估瀏覽器到底會 reflow 哪一部分的代碼,它們都彼此相互影響著。

          重繪(repaint):改變某個元素的背景色、文字顏色、邊框顏色等等不影響它周圍或內部布局的屬性時,屏幕的一部分要重畫,但是元素的幾何尺寸沒有變。

          每次Reflow,Repaint后瀏覽器還需要合并渲染層并輸出到屏幕上。所有的這些都會是動畫卡頓的原因。Reflow 的成本比 Repaint 的成本高得多的多。一個結點的 Reflow 很有可能導致子結點,甚至父點以及同級結點的 Reflow 。在一些高性能的電腦上也許還沒什么,但是如果 Reflow 發生在手機上,那么這個過程是延慢加載和耗電的。可以在csstrigger上查找某個css屬性會觸發什么事件。

          reflow與repaint的時機:

          1. display:none 會觸發 reflow,而 visibility:hidden 只會觸發 repaint,因為沒有發生位置變化。

          2. 有些情況下,比如修改了元素的樣式,瀏覽器并不會立刻 reflow 或 repaint 一次,而是會把這樣的操作積攢一批,然后做一次 reflow,這又叫異步 reflow 或增量異步 reflow。

          3. 有些情況下,比如 resize 窗口,改變了頁面默認的字體等。對于這些操作,瀏覽器會馬上進行 reflow。

          關鍵渲染路徑與阻塞渲染

          在瀏覽器拿到HTML、CSS、JS等外部資源到渲染出頁面的過程,有一個重要的概念關鍵渲染路徑(Critical Rendering Path)。例如為了保障首屏內容的最快速顯示,通常會提到一個漸進式頁面渲染,但是為了漸進式頁面渲染,就需要做資源的拆分,那么以什么粒度拆分、要不要拆分,不同頁面、不同場景策略不同。具體方案的確定既要考慮體驗問題,也要考慮工程問題。了解原理可以讓我們更好的優化關鍵渲染路徑,從而獲得更好的用戶體驗。

          現代瀏覽器總是并行加載資源,例如,當 HTML 解析器(HTML Parser)被腳本阻塞時,解析器雖然會停止構建 DOM,但仍會識別該腳本后面的資源,并進行預加載。

          同時,由于下面兩點:

          1. CSS 被視為渲染 阻塞資源 (包括JS) ,這意味著瀏覽器將不會渲染任何已處理的內容,直至 CSSOM 構建完畢,才會進行下一階段。

          2. JavaScript 被認為是解釋器阻塞資源,HTML解析會被JS阻塞,它不僅可以讀取和修改 DOM 屬性,還可以讀取和修改 CSSOM 屬性。

          存在阻塞的 CSS 資源時,瀏覽器會延遲 JavaScript 的執行和 DOM 構建。另外:

          1. 當瀏覽器遇到一個 script 標記時,DOM 構建將暫停,直至腳本完成執行。

          2. JavaScript 可以查詢和修改 DOM 與 CSSOM。

          3. CSSOM 構建時,JavaScript 執行將暫停,直至 CSSOM 就緒。

          所以,script 標簽的位置很重要。實際使用時,可以遵循下面兩個原則:

          1. CSS 優先:引入順序上,CSS 資源先于 JavaScript 資源。

          2. JavaScript 應盡量少影響 DOM 的構建。

          下面來看看 CSS 與 JavaScript 是具體如何阻塞資源的。

          3.1 CSS

          <style> p { color: red; }</style>

          <link rel="stylesheet" href="index.css">

          這樣的 link 標簽(無論是否 inline)會被視為阻塞渲染的資源,瀏覽器會優先處理這些 CSS 資源,直至 CSSOM 構建完畢。

          渲染樹(Render-Tree)的關鍵渲染路徑中,要求同時具有 DOM 和 CSSOM,之后才會構建渲染樹。即,HTML 和 CSS 都是阻塞渲染的資源。HTML 顯然是必需的,因為包括我們希望顯示的文本在內的內容,都在 DOM 中存放,那么可以從 CSS 上想辦法。

          最容易想到的當然是精簡 CSS 并盡快提供它。除此之外,還可以用媒體類型(media type)和媒體查詢(media query)來解除對渲染的阻塞。

          <link href="index.css" rel="stylesheet">

          <link href="print.css" rel="stylesheet" media="print">

          <link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">

          第一個資源會加載并阻塞。第二個資源設置了媒體類型,會加載但不會阻塞,print 聲明只在打印網頁時使用。第三個資源提供了媒體查詢,會在符合條件時阻塞渲染。

          關于CSS加載的阻塞情況:

          1. css加載不會阻塞DOM樹的解析

          2. css加載會阻塞DOM樹的渲染

          3. css加載會阻塞后面js語句的執行

          沒有js的理想情況下,html與css會并行解析,分別生成DOM與CSSOM,然后合并成Render Tree,進入Rendering Pipeline;但如果有js,css加載會阻塞后面js語句的執行,而(同步)js腳本執行會阻塞其后的DOM解析(所以通常會把css放在頭部,js放在body尾)

          3.2 JavaScript

          JavaScript 的情況比 CSS 要更復雜一些。如果沒有 defer 或 async,瀏覽器會立即加載并執行指定的腳本,“立即”指的是在渲染該 script 標簽之下的HTML元素之前,也就是說不等待后續載入的HTML元素,讀到就加載并執行。觀察下面的代碼:

          <p>Do not go gentle into that good night,</p>

          <script>console.log("inline1")</script>

          <p>Old age should burn and rave at close of day;</p>

          <script src="app.js"></script>

          <p>Rage, rage against the dying of the light.</p>

          <script src="app.js"></script>

          <p>Old age should burn and rave at close of day;</p>

          <script>console.log("inline2")</script>

          <p>Rage, rage against the dying of the light.</p>

          這里的 script 標簽會阻塞 HTML 解析,無論是不是 inline-script。上面的 P 標簽會從上到下解析,這個過程會被兩段 JavaScript 分別打斷一次(加載、執行)。

          解析過程中無論遇到的JavaScript是內聯還是外鏈,只要瀏覽器遇到 script 標記,喚醒 JavaScript解析器,就會進行暫停 (blocked )瀏覽器解析HTML,并等到 CSSOM 構建完畢,才去執行js腳本。因為腳本中可能會操作DOM元素,而如果在加載執行腳本的時候DOM元素并沒有被解析,腳本就會因為DOM元素沒有生成取不到響應元素,所以實際工程中,我們常常將資源放到文檔底部。

          3.3 改變腳本加載次序defer與async

          defer 與 async 可以改變之前的那些阻塞情形,這兩個屬性都會使 script 異步加載,然而執行的時機是不一樣的。注意 async 與 defer 屬性對于 inline-script 都是無效的,所以下面這個示例中三個 script 標簽的代碼會從上到下依次執行。

          <script async>console.log("1")</script>

          <script defer>console.log("2")</script>

          <script>console.log("3")</script>

          上面腳本會按需輸出 1 2 3,故,下面兩節討論的內容都是針對設置了 src 屬性的 script 標簽。

          先放個熟悉的圖~

          藍色線代表網絡讀取,紅色線代表執行時間,這倆都是針對腳本的;綠色線代表 HTML 解析。

          defer:

          <script src="app1.js" defer></script>

          <script src="app2.js" defer></script>

          <script src="app3.js" defer></script>

          defer 屬性表示延遲執行引入 JavaScript,即 JavaScript 加載時 HTML 并未停止解析,這兩個過程是并行的。整個 document 解析完畢且 defer-script 也加載完成之后(這兩件事情的順序無關),會執行所有由 defer-script 加載的 JavaScript 代碼,再觸發 DOMContentLoaded(初始的 HTML 文檔被完全加載和解析完成之后觸發,無需等待樣式表圖像和子框架的完成加載) 事件 。

          defer 不會改變 script 中代碼的執行順序,示例代碼會按照 1、2、3 的順序執行。所以,defer 與相比普通 script,有兩點區別:載入 JavaScript 文件時不阻塞 HTML 的解析,執行階段被放到 HTML 標簽解析完成之后。

          async:

          async 屬性表示異步執行引入的 JavaScript,與 defer 的區別在于,如果已經加載好,就會開始執行,無論此刻是 HTML 解析階段還是 DOMContentLoaded 觸發(HTML解析完成事件)之后。需要注意的是,這種方式加載的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發之前或之后執行,但一定在 load 觸發之前執行。

          從上一段也能推出,多個 async-script 的執行順序是不確定的,誰先加載完誰執行。值得注意的是,向 document 動態添加 script 標簽時,async 屬性默認是 true。

          document.createElement:

          使用 document.createElement 創建的 script 默認是異步的,示例如下。

          console.log(document.createElement("script").async); // true

          所以,通過動態添加 script 標簽引入 JavaScript 文件默認是不會阻塞頁面的。如果想同步執行,需要將 async 屬性人為設置為 false。

          如果使用 document.createElement 創建 link 標簽會怎樣呢?

          const style = document.createElement("link");

          style.rel = "stylesheet";

          style.href = "index.css";

          document.head.appendChild(style); // 阻塞?

          其實這只能通過試驗確定,已知的是,Chrome 中已經不會阻塞渲染,Firefox、IE 在以前是阻塞的,現在會怎樣目前不太清楚。

          優化渲染性能

          結合渲染流程,可以針對性的優化渲染性能:

          1. 優化JS的執行效率

          2. 降低樣式計算的范圍和復雜度

          3. 避免大規模、復雜的布局

          4. 簡化繪制的復雜度、減少繪制區域

          5. 優先使用渲染層合并屬性、控制層數量

          6. 對用戶輸入事件的處理函數去抖動(移動設備)

          這里主要參考Google的瀏覽器渲染性能的基礎講座,想看更詳細內容可以去瞅瞅~

          4.1 優化JS的執行效率

          1. 動畫實現使用requestAnimationFrame

          setTimeout(callback)和setInterval(callback)無法保證callback函數的執行時機,很可能在幀結束的時候執行,從而導致丟幀,如下圖:

          requestAnimationFrame(callback)可以保證callback函數在每幀動畫開始的時候執行。注意:jQuery3.0.0以前版本的animate函數就是用setTimeout來實現動畫,可以通過jquery-requestAnimationFrame這個補丁來用requestAnimationFrame替代setTimeout

          2. 長耗時的JS代碼放到Web Workers中執行

          JS代碼運行在瀏覽器的主線程上,與此同時,瀏覽器的主線程還負責樣式計算、布局、繪制的工作,如果JavaScript代碼運行時間過長,就會阻塞其他渲染工作,很可能會導致丟幀。前面提到每幀的渲染應該在16ms內完成,但在動畫過程中,由于已經被占用了不少時間,所以JavaScript代碼運行耗時應該控制在3-4毫秒。如果真的有特別耗時且不操作DOM元素的純計算工作,可以考慮放到Web Workers中執行。

          var dataSortWorker = new Worker("sort-worker.js");

          dataSortWorker.postMesssage(dataToSort);

          // 主線程不受Web Workers線程干擾

          dataSortWorker.addEventListener('message', function(evt) {

          var sortedData = e.data;

          // Web Workers線程執行結束

          // ...

          });

          3. 拆分操作DOM元素的任務,分別在多個frame完成

          由于Web Workers不能操作DOM元素的限制,所以只能做一些純計算的工作,對于很多需要操作DOM元素的邏輯,可以考慮分步處理,把任務分為若干個小任務,每個任務都放到 requestAnimationFrame中回調執行。

          var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);

          requestAnimationFrame(processTaskList);

          function processTaskList(taskStartTime) {

          var nextTask = taskList.pop;

          // 執行小任務

          processTask(nextTask);

          if (taskList.length > 0) {

          requestAnimationFrame(processTaskList);

          }

          }

          4. 使用Chrome DevTools的Timeline來分析JavaScript的性能

          打開 ChromeDevTools>Timeline>JSProfile,錄制一次動作,然后分析得到的細節信息,從而發現問題并修復問題。

          4.2 降低樣式計算的范圍和復雜度

          添加或移除一個DOM元素、修改元素屬性和樣式類、應用動畫效果等操作,都會引起DOM結構的改變,從而導致瀏覽器要repaint或者reflow。那么這里可以采取一些措施。

          1. 降低樣式選擇器的復雜度

          盡量保持class的簡短,或者使用Web Components框架。

          .box:nth-last-child(-n+1) .title {}

          // 改善后

          .final-box-title {}

          2. 減少需要執行樣式計算的元素個數

          由于瀏覽器的優化,現代瀏覽器的樣式計算直接對目標元素執行,而不是對整個頁面執行,所以我們應該盡可能減少需要執行樣式計算的元素的個數。

          4.3 避免大規模、復雜的布局

          布局就是計算DOM元素的大小和位置的過程,如果你的頁面中包含很多元素,那么計算這些元素的位置將耗費很長時間。布局的主要消耗在于:1. 需要布局的DOM元素的數量;2. 布局過程的復雜程度

          1. 盡可能避免觸發布局

          當你修改了元素的屬性之后,瀏覽器將會檢查為了使這個修改生效是否需要重新計算布局以及更新渲染樹,對于DOM元素的幾何屬性修改,比如width/height/left/top等,都需要重新計算布局。對于不能避免的布局,可以使用Chrome DevTools工具的Timeline查看布局的耗時,以及受影響的DOM元素數量。

          2. 使用flexbox替代老的布局模型

          老的布局模型以相對/絕對/浮動的方式將元素定位到屏幕上,而Floxbox布局模型用流式布局的方式將元素定位到屏幕上。通過一個小實驗可以看出兩種布局模型的性能差距,同樣對1300個元素布局,浮動布局耗時14.3ms,Flexbox布局耗時3.5ms。IE10+支持。

          3. 避免強制同步布局事件的發生

          根據渲染流程,JS腳本是在layout之前執行,但是我們可以強制瀏覽器在執行JS腳本之前先執行布局過程,這就是所謂的強制同步布局。

          requestAnimationFrame(logBoxHeight);

          // 先寫后讀,觸發強制布局

          function logBoxHeight {

          // 更新box樣式

          box.classList.add('super-big');

          // 為了返回box的offersetHeight值

          // 瀏覽器必須先應用屬性修改,接著執行布局過程

          console.log(box.offsetHeight);

          }

          // 先讀后寫,避免強制布局

          function logBoxHeight {

          // 獲取box.offsetHeight

          console.log(box.offsetHeight);

          // 更新box樣式

          box.classList.add('super-big');

          }

          在JS腳本運行的時候,它能獲取到的元素樣式屬性值都是上一幀畫面的,都是舊的值。因此,如果你在當前幀獲取屬性之前又對元素節點有改動,那就會導致瀏覽器必須先應用屬性修改,結果執行布局過程,最后再執行JS邏輯。

          4. 避免連續的強制同步布局發生

          如果連續快速的多次觸發強制同步布局,那么結果更糟糕。比如下面的例子,獲取box的屬性,設置到paragraphs上,由于每次設置paragraphs都會觸發樣式計算和布局過程,而下一次獲取box的屬性必須等到上一步設置結束之后才能觸發。

          function resizeWidth {

          // 會讓瀏覽器陷入'讀寫讀寫'循環

          for (var i = 0; i < paragraphs.length; i++)

          paragraphs[i].style.width = box.offsetWidth + 'px';

          }

          }

          // 改善后方案

          var width = box.offsetWidth;

          function resizeWidth {

          for (var i = 0; i < paragraphs.length; i++)

          paragraphs[i].style.width = width +px';

          }

          }

          注意:可以使用FastDOM來確保讀寫操作的安全,從而幫你自動完成讀寫操作的批處理,還能避免意外地觸發強制同步布局或快速連續布局,消除大量操作DOM的時候的布局抖動。

          4.4 簡化繪制的復雜度、減少繪制區域

          Paint就是填充像素的過程,通常這個過程是整個渲染流程中耗時最長的一環,因此也是最需要避免發生的一環。如果Layout被觸發,那么接下來元素的Paint一定會被觸發。當然純粹改變元素的非幾何屬性,也可能會觸發Paint,比如背景、文字顏色、陰影效果等。

          1. 提升移動或漸變元素的繪制層

          繪制并非總是在內存中的單層畫面里完成的,實際上,瀏覽器在必要時會將一幀畫面繪制成多層畫面,然后將這若干層畫面合并成一張圖片顯示到屏幕上。這種繪制方式的好處是,使用transform來實現移動效果的元素將會被正常繪制,同時不會觸發其他元素的繪制。

          2. 減少繪制區域,簡化繪制的復雜度

          瀏覽器會把相鄰區域的渲染任務合并在一起進行,所以需要對動畫效果進行精密設計,以保證各自的繪制區域不會有太多重疊。另外可以實現同樣效果的不同方式,應該采用性能更好的那種。

          3. 通過Chrome DevTools來分析繪制復雜度和時間消耗,盡可能降低這些指標

          打開DevTools,在彈出的面板中,選中 MoreTools>Rendering選項卡下的Paint flashing,這樣每當頁面發生繪制的時候,屏幕就會閃現綠色的方框。通過該工具可以檢查Paint發生的區域和時機是不是可以被優化。通過Chrome DevTools中的 Timeline>Paint選項可以查看更細節的Paint信息

          4.5 優先使用渲染層合并屬性、控制層數量

          1. 使用transform/opacity實現動畫效果

          使用transform/opacity實現動畫效果,會跳過渲染流程的布局和繪制環節,只做渲染層的合并。

          TypeFunc

          Position

          transform: translate(-px,-px)

          Scale

          transform: scale(-)

          Rotation

          transform: rotate(-deg)

          Skew

          transform: skew(X/Y)(-deg)

          Matrix

          transform: matrix(3d)(..)

          Opacity

          opacity: 0-1

          使用transform/opacity的元素必須獨占一個渲染層,所以必須提升該元素到單獨的渲染層。

          2. 提升動畫效果中的元素

          應用動畫效果的元素應該被提升到其自有的渲染層,但不要濫用。在頁面中創建一個新的渲染層最好的方式就是使用CSS屬性will-change,對于目前還不支持will-change屬性、但支持創建渲染層的瀏覽器,可以通過3D transform屬性來強制瀏覽器創建一個新的渲染層。需要注意的是,不要創建過多的渲染層,這意味著新的內存分配和更復雜的層管理。注意,IE11,Edge17都不支持這一屬性。

          .moving-element {

          will-change: transform;

          transform: translateZ(0);

          }

          3. 管理渲染層、避免過多數量的層

          盡管提升渲染層看起來很誘人,但不能濫用,因為更多的渲染層意味著更多的額外的內存和管理資源,所以當且僅當需要的時候才為元素創建渲染層。

          * {

          will-change: transform;

          transform: translateZ(0);

          }

          4. 使用Chrome DevTools來了解頁面的渲染層情況

          開啟 Timeline>Paint選項,然后錄制一段時間的操作,選擇單獨的幀,看到每個幀的渲染細節,在ESC彈出框有個Layers選項,可以看到渲染層的細節,有多少渲染層,為何被創建?

          4.6 對用戶輸入事件的處理函數去抖動(移動設備)

          用戶輸入事件處理函數會在運行時阻塞幀的渲染,并且會導致額外的布局發生。

          1. 避免使用運行時間過長的輸入事件處理函數

          理想情況下,當用戶和頁面交互,頁面的渲染層合并線程將接收到這個事件并移動元素。這個響應過程是不需要主線程參與,不會導致JavaScript、布局和繪制過程發生。但是如果被觸摸的元素綁定了輸入事件處理函數,比如touchstart/touchmove/touchend,那么渲染層合并線程必須等待這些被綁定的處理函數執行完畢才能執行,也就是用戶的滾動頁面操作被阻塞了,表現出的行為就是滾動出現延遲或者卡頓。

          簡而言之就是你必須確保用戶輸入事件綁定的任何處理函數都能夠快速的執行完畢,以便騰出時間來讓渲染層合并線程完成他的工作。

          2. 避免在輸入事件處理函數中修改樣式屬性

          輸入事件處理函數,比如scroll/touch事件的處理,都會在requestAnimationFrame之前被調用執行。因此,如果你在上述輸入事件的處理函數中做了修改樣式屬性的操作,那么這些操作就會被瀏覽器暫存起來,然后在調用requestAnimationFrame的時候,如果你在一開始就做了讀取樣式屬性的操作,那么將會觸發瀏覽器的強制同步布局操作。

          3. 對滾動事件處理函數去抖動

          通過requestAnimationFrame可以對樣式修改操作去抖動,同時也可以使你的事件處理函數變得更輕。

          function onScroll(evt) {

          // Store the scroll value for laterz.

          lastScrollY = window.scrollY;

          // Prevent multiple rAF callbacks.

          if (scheduledAnimationFrame) {

          return;

          }

          scheduledAnimationFrame = true;

          requestAnimationFrame(readAndUpdatePage);

          }

          window.addEventListener('scroll', onScroll);

          作者簡介:SHERlocked93,來自南京的前端程序員,本碩畢業于北京理工大學,熱愛分享,個人公眾號「前端下午茶」,期待在這里和大家共同進步 ~

          多數設備的刷新頻率是60Hz,也就說是瀏覽器對每一幀畫面的渲染工作要在16ms內完成,超出這個時間,頁面的渲染就會出現卡頓現象,影響用戶體驗。前端的用戶體驗給了前端直觀的印象,因此對B/S架構的開發人員來說,熟悉瀏覽器的內部執行原理顯得尤為重要。

          1. 瀏覽器主要組成與瀏覽器線程

          1.1 瀏覽器組件

          瀏覽器大體上由以下幾個組件組成,各個瀏覽器可能有一點不同。

          • 界面控件 – 包括地址欄,前進后退,書簽菜單等窗口上除了網頁顯示區域以外的部分
          • 瀏覽器引擎 – 查詢與操作渲染引擎的接口
          • 渲染引擎 – 負責顯示請求的內容。比如請求到HTML, 它會負責解析HTML、CSS并將結果顯示到窗口中
          • 網絡 – 用于網絡請求, 如HTTP請求。它包括平臺無關的接口和各平臺獨立的實現
          • UI后端 – 繪制基礎元件,如組合框與窗口。它提供平臺無關的接口,內部使用操作系統的相應實現
          • JS解釋器 - 用于解析執行JavaScript代碼
          • 數據存儲持久層 - 瀏覽器需要把所有數據存到硬盤上,如cookies。新的HTML5規范規定了一個完整(雖然輕量級)的瀏覽器中的數據庫 web database

          注意:chrome瀏覽器與其他瀏覽器不同,chrome使用多個渲染引擎實例,每個Tab頁一個,即每個Tab都是一個獨立進程。

          1.2 瀏覽器中的進程與線程

          Chrome瀏覽器使用多個進程來隔離不同的網頁,在Chrome中打開一個網頁相當于起了一個進程,每個tab網頁都有由其獨立的渲染引擎實例。因為如果非多進程的話,如果瀏覽器中的一個tab網頁崩潰,將會導致其他被打開的網頁應用。另外相對于線程,進程之間是不共享資源和地址空間的,所以不會存在太多的安全問題,而由于多個線程共享著相同的地址空間和資源,所以會存在線程之間有可能會惡意修改或者獲取非授權數據等復雜的安全問題。

          在內核控制下各線程相互配合以保持同步,一個瀏覽器通常由以下常駐線程組成:

          1. GUI 渲染線程

          GUI渲染線程負責渲染瀏覽器界面HTML元素,當界面需要重繪(Repaint)或由于某種操作引發回流(reflow)時,該線程就會執行。在Javascript引擎運行腳本期間,GUI渲染線程都是處于掛起狀態的,也就是說被凍結了.

          2. JavaScript引擎線程

          JS為處理頁面中用戶的交互,以及操作DOM樹、CSS樣式樹來給用戶呈現一份動態而豐富的交互體驗和服務器邏輯的交互處理。如果JS是多線程的方式來操作這些UI DOM,則可能出現UI操作的沖突;如果JS是多線程的話,在多線程的交互下,處于UI中的DOM節點就可能成為一個臨界資源,假設存在兩個線程同時操作一個DOM,一個負責修改一個負責刪除,那么這個時候就需要瀏覽器來裁決如何生效哪個線程的執行結果,當然我們可以通過鎖來解決上面的問題。但為了避免因為引入了鎖而帶來更大的復雜性,JS在最初就選擇了單線程執行。

          GUI渲染線程與JS引擎線程互斥的,是由于JavaScript是可操縱DOM的,如果在修改這些元素屬性同時渲染界面(即JavaScript線程和UI線程同時運行),那么渲染線程前后獲得的元素數據就可能不一致。當JavaScript引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到引擎線程空閑時立即被執行。由于GUI渲染線程與JS執行線程是互斥的關系,當瀏覽器在執行JS程序的時候,GUI渲染線程會被保存在一個隊列中,直到JS程序執行完成,才會接著執行。因此如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞的感覺。

          3. 定時觸發器線程

          瀏覽器定時計數器并不是由JS引擎計數的, 因為JS引擎是單線程的, 如果處于阻塞線程狀態就會影響記計時的準確, 因此通過單獨線程來計時并觸發定時是更為合理的方案。

          4. 事件觸發線程

          當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理。這些事件可以是當前執行的代碼塊如定時任務、也可來自瀏覽器內核的其他線程如鼠標點擊、AJAX異步請求等,但由于JS的單線程關系所有這些事件都得排隊等待JS引擎處理。

          5. 異步http請求線程

          在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求,將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件放到JS引擎的處理隊列中等待處理。

          2. 渲染過程

          2.1 渲染流程

          用戶請求的HTML文本(text/html)通過瀏覽器的網絡層到達渲染引擎后,渲染工作開始。每次通常渲染不會超過8K的數據塊,其中基礎的渲染流程圖:

          webkit引擎渲染的詳細流程,其他引擎渲染流程稍有不同:

          渲染流程有四個主要步驟:

          1. 解析HTML生成DOM樹 - 渲染引擎首先解析HTML文檔,生成DOM樹
          2. 構建Render樹 - 接下來不管是內聯式,外聯式還是嵌入式引入的CSS樣式會被解析生成CSSOM樹,根據DOM樹與CSSOM樹生成另外一棵用于渲染的樹-渲染樹(Render tree),
          3. 布局Render樹 - 然后對渲染樹的每個節點進行布局處理,確定其在屏幕上的顯示位置
          4. 繪制Render樹 - 最后遍歷渲染樹并用UI后端層將每一個節點繪制出來

          以上步驟是一個漸進的過程,為了提高用戶體驗,渲染引擎試圖盡可能快的把結果顯示給最終用戶。它不會等到所有HTML都被解析完才創建并布局渲染樹。它會在從網絡層獲取文檔內容的同時把已經接收到的局部內容先展示出來。

          2.2 渲染細節

          1. 生成DOM樹

          DOM樹的構建過程是一個深度遍歷過程:當前節點的所有子節點都構建好后才會去構建當前節點的下一個兄弟節點。DOM樹的根節點就是document對象。

          DOM樹的生成過程中可能會被CSS和JS的加載執行阻塞,具體可以參見下一章。當HTML文檔解析過程完畢后,瀏覽器繼續進行標記為deferred模式的腳本加載,然后就是整個解析過程的實際結束觸發DOMContentLoaded事件,并在async文檔文檔執行完之后觸發load事件。

          2. 生成Render樹

          生成DOM樹的同時會生成樣式結構體CSSOM(CSS Object Model)Tree,再根據CSSOM和DOM樹構造渲染樹Render Tree,渲染樹包含帶有顏色,尺寸等顯示屬性的矩形,這些矩形的順序與顯示順序基本一致。從MVC的角度來說,可以將Render樹看成是V,DOM樹與CSSOM樹看成是M,C則是具體的調度者,比HTMLDocumentParser等。

          可以這么說,沒有DOM樹就沒有Render樹,但是它們之間不是簡單的一對一的關系。Render樹是用于顯示,那不可見的元素當然不會在這棵樹中出現了,譬如 <head>。除此之外,display等于none的也不會被顯示在這棵樹里頭,但是visibility等于hidden的元素是會顯示在這棵樹里頭的。

          3. DOM樹與Render樹

          DOM對象類型很豐富,什么head、title、div,而Render樹相對來說就比較單一了,畢竟它的職責就是為了以后的顯示渲染用嘛。Render樹的每一個節點我們叫它渲染器renderer。

          一棵Render樹大概是醬紫,左邊是DOM樹,右邊是Render樹:

          從上圖我們可以看出,renderer與DOM元素是相對應的,但并不是一一對應,有些DOM元素沒有對應的renderer,而有些DOM元素卻對應了好幾個renderer,對應多個renderer的情況是普遍存在的,就是為了解決一個renderer描述不清楚如何顯示出來的問題,譬如有下拉列表的select元素,我們就需要三個renderer:一個用于顯示區域,一個用于下拉列表框,還有一個用于按鈕。

          另外,renderer與DOM元素的位置也可能是不一樣的。那些添加了 float或者 position:absolute的元素,因為它們脫離了正常的文檔流,構造Render樹的時候會針對它們實際的位置進行構造。

          4. 布局與繪制

          上面確定了renderer的樣式規則后,然后就是重要的顯示元素布局了。當renderer構造出來并添加到Render樹上之后,它并沒有位置跟大小信息,為它確定這些信息的過程,接下來是布局(layout)。

          瀏覽器進行頁面布局基本過程是以瀏覽器可見區域為畫布,左上角為 (0,0)基礎坐標,從左到右,從上到下從DOM的根節點開始畫,首先確定顯示元素的大小跟位置,此過程是通過瀏覽器計算出來的,用戶CSS中定義的量未必就是瀏覽器實際采用的量。如果顯示元素有子元素得先去確定子元素的顯示信息。

          布局階段輸出的結果稱為box盒模型(width,height,margin,padding,border,left,top,…),盒模型精確表示了每一個元素的位置和大小,并且所有相對度量單位此時都轉化為了絕對單位。

          繪制(painting)階段,渲染引擎會遍歷Render樹,并調用renderer的 paint() 方法,將renderer的內容顯示在屏幕上。繪制工作是使用UI后端組件完成的。

          5. 回流與重繪

          回流(reflow):當瀏覽器發現某個部分發生了點變化影響了布局,需要倒回去重新渲染。reflow 會從 <html>這個 root frame 開始遞歸往下,依次計算所有的結點幾何尺寸和位置。reflow 幾乎是無法避免的。現在界面上流行的一些效果,比如樹狀目錄的折疊、展開(實質上是元素的顯示與隱藏)等,都將引起瀏覽器的 reflow。鼠標滑過、點擊……只要這些行為引起了頁面上某些元素的占位面積、定位方式、邊距等屬性的變化,都會引起它內部、周圍甚至整個頁面的重新渲染。通常我們都無法預估瀏覽器到底會 reflow 哪一部分的代碼,它們都彼此相互影響著。

          重繪(repaint):改變某個元素的背景色、文字顏色、邊框顏色等等不影響它周圍或內部布局的屬性時,屏幕的一部分要重畫,但是元素的幾何尺寸沒有變。

          每次Reflow,Repaint后瀏覽器還需要合并渲染層并輸出到屏幕上。所有的這些都會是動畫卡頓的原因。Reflow 的成本比 Repaint 的成本高得多的多。一個結點的 Reflow 很有可能導致子結點,甚至父點以及同級結點的 Reflow 。在一些高性能的電腦上也許還沒什么,但是如果 Reflow 發生在手機上,那么這個過程是延慢加載和耗電的。可以在csstrigger上查找某個css屬性會觸發什么事件。

          reflow與repaint的時機

          1. display:none 會觸發 reflow,而 visibility:hidden 只會觸發 repaint,因為沒有發生位置變化。
          2. 有些情況下,比如修改了元素的樣式,瀏覽器并不會立刻 reflow 或 repaint 一次,而是會把這樣的操作積攢一批,然后做一次 reflow,這又叫異步 reflow 或增量異步 reflow。
          3. 有些情況下,比如 resize 窗口,改變了頁面默認的字體等。對于這些操作,瀏覽器會馬上進行 reflow。

          3. 關鍵渲染路徑與阻塞渲染

          在瀏覽器拿到HTML、CSS、JS等外部資源到渲染出頁面的過程,有一個重要的概念關鍵渲染路徑(Critical Rendering Path)。例如為了保障首屏內容的最快速顯示,通常會提到一個漸進式頁面渲染,但是為了漸進式頁面渲染,就需要做資源的拆分,那么以什么粒度拆分、要不要拆分,不同頁面、不同場景策略不同。具體方案的確定既要考慮體驗問題,也要考慮工程問題。了解原理可以讓我們更好的優化關鍵渲染路徑,從而獲得更好的用戶體驗。

          現代瀏覽器總是并行加載資源,例如,當 HTML 解析器(HTML Parser)被腳本阻塞時,解析器雖然會停止構建 DOM,但仍會識別該腳本后面的資源,并進行預加載。

          同時,由于下面兩點:

          1. CSS 被視為渲染 阻塞資源 (包括JS) ,這意味著瀏覽器將不會渲染任何已處理的內容,直至 CSSOM 構建完畢,才會進行下一階段。
          2. JavaScript 被認為是解釋器阻塞資源,HTML解析會被JS阻塞,它不僅可以讀取和修改 DOM 屬性,還可以讀取和修改 CSSOM 屬性。

          存在阻塞的 CSS 資源時,瀏覽器會延遲 JavaScript 的執行和 DOM 構建。另外:

          1. 當瀏覽器遇到一個 script 標記時,DOM 構建將暫停,直至腳本完成執行。
          2. JavaScript 可以查詢和修改 DOM 與 CSSOM。
          3. CSSOM 構建時,JavaScript 執行將暫停,直至 CSSOM 就緒。

          所以,script 標簽的位置很重要。實際使用時,可以遵循下面兩個原則:

          1. CSS 優先:引入順序上,CSS 資源先于 JavaScript 資源。
          2. JavaScript 應盡量少影響 DOM 的構建。

          下面來看看 CSS 與 JavaScript 是具體如何阻塞資源的。

          3.1 CSS

          <style>
            p { color: red; }
          </style>
          <link rel="stylesheet" href="index.css">

          這樣的 link 標簽(無論是否 inline)會被視為阻塞渲染的資源,瀏覽器會優先處理這些 CSS 資源,直至 CSSOM 構建完畢。

          渲染樹(Render-Tree)的關鍵渲染路徑中,要求同時具有 DOM 和 CSSOM,之后才會構建渲染樹。即,HTML 和 CSS 都是阻塞渲染的資源。HTML 顯然是必需的,因為包括我們希望顯示的文本在內的內容,都在 DOM 中存放,那么可以從 CSS 上想辦法。

          最容易想到的當然是精簡 CSS 并盡快提供它。除此之外,還可以用媒體類型(media type)和媒體查詢(media query)來解除對渲染的阻塞。

          <link href="index.css" rel="stylesheet">
          <link href="print.css" rel="stylesheet" media="print">
          <link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">

          第一個資源會加載并阻塞。第二個資源設置了媒體類型,會加載但不會阻塞,print 聲明只在打印網頁時使用。第三個資源提供了媒體查詢,會在符合條件時阻塞渲染。

          關于CSS加載的阻塞情況:

          1. css加載不會阻塞DOM樹的解析
          2. css加載會阻塞DOM樹的渲染
          3. css加載會阻塞后面js語句的執行

          沒有js的理想情況下,html與css會并行解析,分別生成DOM與CSSOM,然后合并成Render Tree,進入Rendering Pipeline;但如果有js,css加載會阻塞后面js語句的執行,而(同步)js腳本執行會阻塞其后的DOM解析(所以通常會把css放在頭部,js放在body尾)

          3.2 JavaScript

          JavaScript 的情況比 CSS 要更復雜一些。如果沒有 defer 或 async,瀏覽器會立即加載并執行指定的腳本,“立即”指的是在渲染該 script 標簽之下的HTML元素之前,也就是說不等待后續載入的HTML元素,讀到就加載并執行。觀察下面的代碼:

          <p>Do not go gentle into that good night,</p>
          
          <script>console.log("inline1")</script>
          <p>Old age should burn and rave at close of day;</p>
          
          <script src="app.js"></script>
          <p>Rage, rage against the dying of the light.</p>
          
          <script src="app.js"></script>
          <p>Old age should burn and rave at close of day;</p>
          
          <script>console.log("inline2")</script>
          <p>Rage, rage against the dying of the light.</p>

          這里的 script 標簽會阻塞 HTML 解析,無論是不是 inline-script。上面的 P 標簽會從上到下解析,這個過程會被兩段 JavaScript 分別打斷一次(加載、執行)。

          解析過程中無論遇到的JavaScript是內聯還是外鏈,只要瀏覽器遇到 script 標記,喚醒 JavaScript解析器,就會進行暫停 (blocked )瀏覽器解析HTML,并等到 CSSOM 構建完畢,才去執行js腳本。因為腳本中可能會操作DOM元素,而如果在加載執行腳本的時候DOM元素并沒有被解析,腳本就會因為DOM元素沒有生成取不到響應元素,所以實際工程中,我們常常將資源放到文檔底部。

          3.3 改變腳本加載次序defer與async

          defer 與 async 可以改變之前的那些阻塞情形,這兩個屬性都會使 script 異步加載,然而執行的時機是不一樣的。注意 async 與 defer 屬性對于 inline-script 都是無效的,所以下面這個示例中三個 script 標簽的代碼會從上到下依次執行。

          <script async>
            console.log("1")
          </script>
          
          <script defer>
            console.log("2")
          </script>
          
          <script>
            console.log("3")
          </script>

          上面腳本會按需輸出 1 2 3,故,下面兩節討論的內容都是針對設置了 src 屬性的 script 標簽。

          先放個熟悉的圖~

          藍色線代表網絡讀取,紅色線代表執行時間,這倆都是針對腳本的;綠色線代表 HTML 解析。

          defer

          <script src="app1.js" defer></script>
          <script src="app2.js" defer></script>
          <script src="app3.js" defer></script>

          defer 屬性表示延遲執行引入 JavaScript,即 JavaScript 加載時 HTML 并未停止解析,這兩個過程是并行的。整個 document 解析完畢且 defer-script 也加載完成之后(這兩件事情的順序無關),會執行所有由 defer-script 加載的 JavaScript 代碼,再觸發 DOMContentLoaded(初始的 HTML 文檔被完全加載和解析完成之后觸發,無需等待樣式表圖像和子框架的完成加載) 事件 。

          defer 不會改變 script 中代碼的執行順序,示例代碼會按照 1、2、3 的順序執行。所以,defer 與相比普通 script,有兩點區別:載入 JavaScript 文件時不阻塞 HTML 的解析,執行階段被放到 HTML 標簽解析完成之后。

          async

          async 屬性表示異步執行引入的 JavaScript,與 defer 的區別在于,如果已經加載好,就會開始執行,無論此刻是 HTML 解析階段還是 DOMContentLoaded 觸發(HTML解析完成事件)之后。需要注意的是,這種方式加載的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發之前或之后執行,但一定在 load 觸發之前執行。

          從上一段也能推出,多個 async-script 的執行順序是不確定的,誰先加載完誰執行。值得注意的是,向 document 動態添加 script 標簽時,async 屬性默認是 true。

          document.createElement

          使用 document.createElement 創建的 script 默認是異步的,示例如下。

          console.log(document.createElement("script").async); // true

          所以,通過動態添加 script 標簽引入 JavaScript 文件默認是不會阻塞頁面的。如果想同步執行,需要將 async 屬性人為設置為 false。

          如果使用 document.createElement 創建 link 標簽會怎樣呢?

          const style = document.createElement("link");
          style.rel = "stylesheet";
          style.href = "index.css";
          document.head.appendChild(style); // 阻塞?

          其實這只能通過試驗確定,已知的是,Chrome 中已經不會阻塞渲染,Firefox、IE 在以前是阻塞的,現在會怎樣目前不太清楚。

          4. 優化渲染性能

          結合渲染流程,可以針對性的優化渲染性能:

          1. 優化JS的執行效率
          2. 降低樣式計算的范圍和復雜度
          3. 避免大規模、復雜的布局
          4. 簡化繪制的復雜度、減少繪制區域
          5. 優先使用渲染層合并屬性、控制層數量
          6. 對用戶輸入事件的處理函數去抖動(移動設備)

          這里主要參考Google的瀏覽器渲染性能的基礎講座,想看更詳細內容可以去瞅瞅~

          4.1 優化JS的執行效率

          1. 動畫實現使用requestAnimationFrame

          setTimeout(callback)和setInterval(callback)無法保證callback函數的執行時機,很可能在幀結束的時候執行,從而導致丟幀,如下圖:

          requestAnimationFrame(callback)可以保證callback函數在每幀動畫開始的時候執行。注意:jQuery3.0.0以前版本的animate函數就是用setTimeout來實現動畫,可以通過jquery-requestAnimationFrame這個補丁來用requestAnimationFrame替代setTimeout

          2. 長耗時的JS代碼放到Web Workers中執行

          JS代碼運行在瀏覽器的主線程上,與此同時,瀏覽器的主線程還負責樣式計算、布局、繪制的工作,如果JavaScript代碼運行時間過長,就會阻塞其他渲染工作,很可能會導致丟幀。前面提到每幀的渲染應該在16ms內完成,但在動畫過程中,由于已經被占用了不少時間,所以JavaScript代碼運行耗時應該控制在3-4毫秒。如果真的有特別耗時且不操作DOM元素的純計算工作,可以考慮放到Web Workers中執行。

          var dataSortWorker = new Worker("sort-worker.js");
          dataSortWorker.postMesssage(dataToSort);
          // 主線程不受Web Workers線程干擾
          dataSortWorker.addEventListener('message',                                function(evt) {    
            var sortedData = e.data;
              // Web Workers線程執行結束    // ...});

          3. 拆分操作DOM元素的任務,分別在多個frame完成

          由于Web Workers不能操作DOM元素的限制,所以只能做一些純計算的工作,對于很多需要操作DOM元素的邏輯,可以考慮分步處理,把任務分為若干個小任務,每個任務都放到 requestAnimationFrame中回調執行

          var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
          requestAnimationFrame(processTaskList);
          function processTaskList(taskStartTime) {
            var nextTask = taskList.pop();
              // 執行小任務    
            processTask(nextTask);
            if (taskList.length > 0) {
              requestAnimationFrame(processTaskList);
            }
          }

          4. 使用Chrome DevTools的Timeline來分析JavaScript的性能

          打開 ChromeDevTools>Timeline>JSProfile,錄制一次動作,然后分析得到的細節信息,從而發現問題并修復問題。

          4.2 降低樣式計算的范圍和復雜度

          添加或移除一個DOM元素、修改元素屬性和樣式類、應用動畫效果等操作,都會引起DOM結構的改變,從而導致瀏覽器要repaint或者reflow。那么這里可以采取一些措施。

          1. 降低樣式選擇器的復雜度

          盡量保持class的簡短,或者使用Web Components框架。

          .box:nth-last-child(-n+1) .title {}
          // 改善后
          .final-box-title {}

          2. 減少需要執行樣式計算的元素個數

          由于瀏覽器的優化,現代瀏覽器的樣式計算直接對目標元素執行,而不是對整個頁面執行,所以我們應該盡可能減少需要執行樣式計算的元素的個數。

          4.3 避免大規模、復雜的布局

          布局就是計算DOM元素的大小和位置的過程,如果你的頁面中包含很多元素,那么計算這些元素的位置將耗費很長時間。布局的主要消耗在于:1. 需要布局的DOM元素的數量;2. 布局過程的復雜程度

          1. 盡可能避免觸發布局

          當你修改了元素的屬性之后,瀏覽器將會檢查為了使這個修改生效是否需要重新計算布局以及更新渲染樹,對于DOM元素的幾何屬性修改,比如width/height/left/top等,都需要重新計算布局。對于不能避免的布局,可以使用Chrome DevTools工具的Timeline查看布局的耗時,以及受影響的DOM元素數量。

          2. 使用flexbox替代老的布局模型

          老的布局模型以相對/絕對/浮動的方式將元素定位到屏幕上,而Floxbox布局模型用流式布局的方式將元素定位到屏幕上。通過一個小實驗可以看出兩種布局模型的性能差距,同樣對1300個元素布局,浮動布局耗時14.3ms,Flexbox布局耗時3.5ms。IE10+支持。

          3. 避免強制同步布局事件的發生

          根據渲染流程,JS腳本是在layout之前執行,但是我們可以強制瀏覽器在執行JS腳本之前先執行布局過程,這就是所謂的強制同步布局。

          requestAnimationFrame(logBoxHeight);
          // 先寫后讀,觸發強制布局
          function logBoxHeight() {
            // 更新box樣式    
            box.classList.add('super-big');
            // 為了返回box的offersetHeight值
            // 瀏覽器必須先應用屬性修改,接著執行布局過程
            console.log(box.offsetHeight);
          }
          
          // 先讀后寫,避免強制布局
          function logBoxHeight() {
            // 獲取box.offsetHeight
            console.log(box.offsetHeight);
              // 更新box樣式
            box.classList.add('super-big');
          }

          在JS腳本運行的時候,它能獲取到的元素樣式屬性值都是上一幀畫面的,都是舊的值。因此,如果你在當前幀獲取屬性之前又對元素節點有改動,那就會導致瀏覽器必須先應用屬性修改,結果執行布局過程,最后再執行JS邏輯。

          4. 避免連續的強制同步布局發生

          如果連續快速的多次觸發強制同步布局,那么結果更糟糕。比如下面的例子,獲取box的屬性,設置到paragraphs上,由于每次設置paragraphs都會觸發樣式計算和布局過程,而下一次獲取box的屬性必須等到上一步設置結束之后才能觸發。

          function resizeWidth() {    // 會讓瀏覽器陷入'讀寫讀寫'循環    for (var i = 0; i < paragraphs.length; i++) {        paragraphs[i].style.width = box.offsetWidth + 'px';    }}
          // 改善后方案var width = box.offsetWidth;function resizeWidth() {    for (var i = 0; i < paragraphs.length; i++) {        paragraphs[i].style.width = width + 'px';    }}

          注意:可以使用FastDOM來確保讀寫操作的安全,從而幫你自動完成讀寫操作的批處理,還能避免意外地觸發強制同步布局或快速連續布局,消除大量操作DOM的時候的布局抖動。

          4.4 簡化繪制的復雜度、減少繪制區域

          Paint就是填充像素的過程,通常這個過程是整個渲染流程中耗時最長的一環,因此也是最需要避免發生的一環。如果Layout被觸發,那么接下來元素的Paint一定會被觸發。當然純粹改變元素的非幾何屬性,也可能會觸發Paint,比如背景、文字顏色、陰影效果等。

          1. 提升移動或漸變元素的繪制層

          繪制并非總是在內存中的單層畫面里完成的,實際上,瀏覽器在必要時會將一幀畫面繪制成多層畫面,然后將這若干層畫面合并成一張圖片顯示到屏幕上。這種繪制方式的好處是,使用transform來實現移動效果的元素將會被正常繪制,同時不會觸發其他元素的繪制。

          2. 減少繪制區域,簡化繪制的復雜度

          瀏覽器會把相鄰區域的渲染任務合并在一起進行,所以需要對動畫效果進行精密設計,以保證各自的繪制區域不會有太多重疊。另外可以實現同樣效果的不同方式,應該采用性能更好的那種。

          3. 通過Chrome DevTools來分析繪制復雜度和時間消耗,盡可能降低這些指標

          打開DevTools,在彈出的面板中,選中 MoreTools>Rendering選項卡下的Paint flashing,這樣每當頁面發生繪制的時候,屏幕就會閃現綠色的方框。通過該工具可以檢查Paint發生的區域和時機是不是可以被優化。通過Chrome DevTools中的 Timeline>Paint選項可以查看更細節的Paint信息

          4.5 優先使用渲染層合并屬性、控制層數量

          1. 使用transform/opacity實現動畫效果

          使用transform/opacity實現動畫效果,會跳過渲染流程的布局和繪制環節,只做渲染層的合并。

          TypeFuncPositiontransform: translate(-px,-px)Scaletransform: scale(-)Rotationtransform: rotate(-deg)Skewtransform: skew(X/Y)(-deg)Matrixtransform: matrix(3d)(..)Opacityopacity: 0-1

          使用transform/opacity的元素必須獨占一個渲染層,所以必須提升該元素到單獨的渲染層。

          2. 提升動畫效果中的元素

          應用動畫效果的元素應該被提升到其自有的渲染層,但不要濫用。在頁面中創建一個新的渲染層最好的方式就是使用CSS屬性will-change,對于目前還不支持will-change屬性、但支持創建渲染層的瀏覽器,可以通過3D transform屬性來強制瀏覽器創建一個新的渲染層。需要注意的是,不要創建過多的渲染層,這意味著新的內存分配和更復雜的層管理。注意,IE11,Edge17都不支持這一屬性。

          .moving-element {    will-change: transform;    transform: translateZ(0);}

          3. 管理渲染層、避免過多數量的層

          盡管提升渲染層看起來很誘人,但不能濫用,因為更多的渲染層意味著更多的額外的內存和管理資源,所以當且僅當需要的時候才為元素創建渲染層。

          * {  will-change: transform;  transform: translateZ(0);}

          4. 使用Chrome DevTools來了解頁面的渲染層情況

          開啟 Timeline>Paint選項,然后錄制一段時間的操作,選擇單獨的幀,看到每個幀的渲染細節,在ESC彈出框有個Layers選項,可以看到渲染層的細節,有多少渲染層,為何被創建?

          4.6 對用戶輸入事件的處理函數去抖動(移動設備)

          用戶輸入事件處理函數會在運行時阻塞幀的渲染,并且會導致額外的布局發生。

          1. 避免使用運行時間過長的輸入事件處理函數

          理想情況下,當用戶和頁面交互,頁面的渲染層合并線程將接收到這個事件并移動元素。這個響應過程是不需要主線程參與,不會導致JavaScript、布局和繪制過程發生。但是如果被觸摸的元素綁定了輸入事件處理函數,比如touchstart/touchmove/touchend,那么渲染層合并線程必須等待這些被綁定的處理函數執行完畢才能執行,也就是用戶的滾動頁面操作被阻塞了,表現出的行為就是滾動出現延遲或者卡頓。

          簡而言之就是你必須確保用戶輸入事件綁定的任何處理函數都能夠快速的執行完畢,以便騰出時間來讓渲染層合并線程完成他的工作。

          2. 避免在輸入事件處理函數中修改樣式屬性

          輸入事件處理函數,比如scroll/touch事件的處理,都會在requestAnimationFrame之前被調用執行。因此,如果你在上述輸入事件的處理函數中做了修改樣式屬性的操作,那么這些操作就會被瀏覽器暫存起來,然后在調用requestAnimationFrame的時候,如果你在一開始就做了讀取樣式屬性的操作,那么將會觸發瀏覽器的強制同步布局操作。

          3. 對滾動事件處理函數去抖動

          通過requestAnimationFrame可以對樣式修改操作去抖動,同時也可以使你的事件處理函數變得更輕


          主站蜘蛛池模板: 一区二区高清视频在线观看| 99久久综合狠狠综合久久一区| 日韩精品免费一区二区三区| 精品人妻一区二区三区毛片| 在线日产精品一区| 亚欧色一区W666天堂| 一区二区三区亚洲| 99精品国产高清一区二区三区| 91福利国产在线观看一区二区| 一区二区三区亚洲视频| 亚洲欧美国产国产一区二区三区| 国产精品无码一区二区在线观| 国产香蕉一区二区三区在线视频| 国产人妖视频一区二区破除 | 日韩一区二区a片免费观看| 精品国产天堂综合一区在线| 波多野结衣一区二区三区| 中文字幕视频一区| 亚洲变态另类一区二区三区| 亚洲av无码片区一区二区三区| 美女福利视频一区| 中文字幕在线观看一区二区三区| 人成精品视频三区二区一区 | 久久精品国产一区二区三区| 国产嫖妓一区二区三区无码| 能在线观看的一区二区三区| 99国产精品欧美一区二区三区| 色欲综合一区二区三区| 国产伦精品一区二区三区女| 韩国精品一区视频在线播放| 国产精品一区二区久久精品| 无码人妻一区二区三区免费视频| 久久久99精品一区二区| 亚洲国产精品第一区二区| 国产丝袜视频一区二区三区| 亚洲天堂一区二区| 琪琪see色原网一区二区| 日韩人妻不卡一区二区三区| 日韩一区二区免费视频| 亚洲国产成人一区二区三区| 国产伦精品一区二区三区免.费|