<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一名合格的前端工程師需要掌握的瀏覽器渲染筆記

          共 18445字,需瀏覽 37分鐘

           ·

          2021-04-08 10:43

          前言

          瀏覽器渲染這是一個廣而深的題目,其中的每一個點(diǎn)如果深入,都可以講一整天。本文主要從廣度的層面,梳理了瀏覽器的整體渲染流程,有不對的地方,煩請指正!

          ps:本文整體思路主要參考極客時間專欄-瀏覽器工作原理與實(shí)踐[1](推薦,講的不錯),文中部分圖片畫起來比較復(fù)雜,也直接采用了文中的圖片,僅供學(xué)習(xí)。

          整體流程

          • 1、解析HTML,構(gòu)建DOM樹
          • 2、解析CSS,生成CSS規(guī)則樹
          • 3、合并DOM樹和CSS規(guī)則,生成Render樹(頁面布局)
          • 4、繪制Render樹(paint),繪制頁面像素信息
          • 5、顯示

          1、構(gòu)建DOM樹

          因?yàn)闉g覽器無法直接理解和使用html,所以需要將html轉(zhuǎn)換為瀏覽器能夠理解的結(jié)構(gòu)——DOM樹。在渲染引擎內(nèi)部,有一個叫 HTML 解析器(HTMLParser)的模塊,它的職責(zé)就是負(fù)責(zé)將 HTML 字節(jié)流轉(zhuǎn)換為 DOM 結(jié)構(gòu)。

          • 1、解碼:瀏覽器將接收的字節(jié)流(Bytes)基于編碼方式解析為字符(characters)
          • 2、分詞:通過分詞器(也就是詞法分析)將字符轉(zhuǎn)換為 Token,分為Tag Token 和文本Token
          • 3、tokens->nodes
          • 4、nodes->DOM

          第3步和第4步其實(shí)是同時進(jìn)行的,需要將 Token 解析為 DOM 節(jié)點(diǎn),并將 DOM 節(jié)點(diǎn)添加到 DOM 樹中。此過程HTML 解析器通過維護(hù)了一個Token棧結(jié)構(gòu)來完成。

          • 如果是StartTag Token,就會創(chuàng)建一個DOM節(jié)點(diǎn),并推入棧
          • 如果是文本 Token,就會生成一個文本節(jié)點(diǎn),然后將直接該節(jié)點(diǎn)加入到 DOM 樹中
          • 如果是EndTag Token,會查看棧頂元素是否為對應(yīng)的StartTag Token,如果是則彈出,該節(jié)點(diǎn)解析完成。

          具體實(shí)現(xiàn)可以參考Vue.js的HTMLParser實(shí)現(xiàn)[2]

          2、構(gòu)建CSS規(guī)則樹

          與HTML文本一樣,渲染引擎也沒法直接理解CSS文本,因此渲染引擎會將其轉(zhuǎn)換為其能理解的結(jié)構(gòu)——styleSheets。在控制臺執(zhí)行document.styleSheets 可以看到:

          styleSheets是對頁面樣式的一個總覽,其內(nèi)部層級如下圖:

          關(guān)于stylesheets的具體屬性,可參考鏈接[3]

          針對styleSheets,結(jié)合CSS的繼承、優(yōu)先級層疊等規(guī)則,渲染引擎最終生成如下CSS規(guī)則樹:

          此時每個元素上的樣式就是最終應(yīng)用這個元素上的樣式了,通過瀏覽器的Element->Computed可以查看。

          3、布局Layout

          頁面結(jié)構(gòu)和頁面樣式都確定了,接下來就需要將兩者結(jié)合起來,對頁面進(jìn)行整體布局。

          3.1構(gòu)建Render樹

          DOM樹只是描述了源碼中HTML的結(jié)構(gòu),但其中許多元素并不需要展示在畫面中(比如head、dispaly:none),也有一些不存在DOM樹中但需要顯示在頁面上的元素(比如偽類),因此在顯示之前需要遍歷DOM樹中的所有節(jié)點(diǎn),忽略掉不可見元素,添加不存在DOM樹中但需要顯示的的內(nèi)容,最終生成一棵只包含可見元素的Render樹。

          3.2計(jì)算布局信息

          以上得到了每個DOM元素的文檔結(jié)構(gòu)和樣式,但是還不知道元素的具體絕對幾何位置。

          比如一個div元素的樣式如下:

          div {
              position: absolute;
              width:100px;
              height:100px;
              top:10px;
              left:10px;
          }

          那么我們還需要知道它的具體絕對幾何位置:

          div {
              x: ?
              y: ?
              width: 100px
              height: 100px
          }

          而計(jì)算元素的具體絕對幾何位置是一項(xiàng)艱巨的任務(wù),因?yàn)榧词故亲詈唵蔚捻撁娌季郑ㄈ鐝纳系较碌膲K流程)也必須考慮字體的大小以及在何處換行,因?yàn)檫@會影響段落的大小和形狀,也會影響下一段的位置。

          在Chrome中,有一整個工程師團(tuán)隊(duì)在為布局而工作, few talks from BlinkOn Conference[4] 有提到一些,大家感興趣可以看看。

          4、繪制paint

          以上得到了完整的Render樹,也就是知道了頁面的樣式和位置信息,但還沒到繪制的時候。類似于畫一幅畫,我們還需要知道頁面各元素的繪制順序,比如需要先畫藍(lán)天再畫白云,否則白云會被藍(lán)天覆蓋住。

          針對繪制順序,因?yàn)轫撁嬷杏泻芏鄰?fù)雜的效果,如一些復(fù)雜的 3D 變換、頁面滾動,或者使用 z-index做 z 軸排序等,為了更加方便地實(shí)現(xiàn)這些效果,渲染引擎采用了分層機(jī)制。

          4.1分層layer

          每個DOM元素會有自己的布局信息Layout Object, 根據(jù)其布局信息的層級等關(guān)系,某些Layout Object會擁有共同的渲染層Paint Layer,某些Paint Layer又會擁有共同的合成層Composite Layer(Graphic Layers)。

          分層-渲染層(Paint Layer)

          如上圖,DOM 樹中得每個 Node 節(jié)點(diǎn)都有一個對應(yīng)的 LayoutObject;擁有相同的坐標(biāo)空間的 LayoutObjects,屬于同一個渲染層(PaintLayer)。渲染層產(chǎn)生的最普遍條件是“層疊上下文”。

          層疊上下文示意圖:

          根據(jù)層疊上下文-MDN[5],層疊上下文由滿足以下任意一個條件的元素形成:

          滿足以上任一條件的元素,都會擁有自己的渲染層,其子元素若沒有單獨(dú)的渲染層,則隨父級元素同一層。

          其他產(chǎn)生渲染層的特殊場景(除“層疊上下文”),可參考鏈接[6]

          分層-合成層(Composite Layer)

          某些特殊的渲染層會被認(rèn)為是合成層(Composite Layer),合成層擁有單獨(dú)的 GraphicsLayer。渲染層與合成層的區(qū)別,如圖:

          產(chǎn)生合成層的具體條件可參考文章,這里列出幾個常見的場景:

          • 有 3D transform
          • 對 opacity、fliter、transform 應(yīng)用了 animation 或者 transition(需要是 active 的 animation 或者 transition,當(dāng) animation 或者 transition 效果未開始或結(jié)束后,提升合成層也會失效)
          • will-change 設(shè)置為 opacity、transform、top、left、bottom、right(其中 top、left 等需要設(shè)置明確的定位屬性,如 relative 等

          以上三種原因生成合成層demo代碼如下

          <!DOCTYPE html>
          <html lang="en">
          <head>
              <style type="text/css">
                  *{
                          margin:0;
                          padding:0;
                  }
                  div{
                          width:200px;
                          height:100px;
                  }
                  .default{
                          background#ffb284;
                  }
                  .composite-translateZ{
                          transformtranslateZ(0);
                          background#f5cec7;
                  }
                  .composite-tansform-active{
                          background#e79796;
                          transformtranslate(0,0);
                          transition3s;
                  }
                  .composite-tansform-active:hover{
                          transformtranslate(100px,100px);        
                  }
                  .composite-will-change{
                          background#ffc988;
                          will-change: transform;
                  }
              
          </style>
          </head>
          <body>
              <div class='default'>默認(rèn)層</div>
              <div class='composite-translateZ'>合成層-translateZ</div>
              <div class='composite-tansform-active'>合成層——active transform(hover一下我)</div>
              <div class='composite-will-change'>合成層——will-change</div>
          </body>
          </html>

          在控制臺的Layers下,可以看到合成層。

          • overlap:元素覆蓋在其他合成層元素上,則該元素被隱式提升為合成層,demo代碼如下
          <!DOCTYPE html>
          <html lang="en">
          <head>
              <style type="text/css">
                  *{
                          margin:0;
                          padding:0;
                  }
                  div{
                          width:200px;
                          height:200px;
                          /*background: */
                  }
                  .bottom{
                          background#f5cec7;
                          animation: anim-translate 3s ease-in-out alternate infinite both;
                  }
                  @keyframes anim-translate {
                      from { transformtranslateX(0); }
                      to { transformtranslateX(50px); }
                  }
                  .top{
                          background#e79796;
                          transformtranslateY(-50px);
                  }       
              
          </style>
          </head>
          <body>
              <div class='parents'>
                  <div class="bottom">下層-有動畫</div>
                  <div class="top">上層-隱式提升為合成層</div>
              </div>
          </body>
          </html>

          demo中的上層div,本不具備提升為合成層的因素,但由于其覆蓋在了下層div上,如果上層div不隱式提升為合成層,它就會和和父元素共用一個合成層,此時渲染順序就會出錯。為了保證渲染順序,因此上層被隱式提升為合成層。在控制臺也可以看到原因:might overlap other composited content.

          渲染層是為保證頁面元素以正確的順序,合成層是為了減少渲染的開銷。

          提升為合成層的好處:

          • 合成層的位圖,會采用硬件加速,也就是會交由 GPU 合成,比 CPU 處理要快
          • 當(dāng)需要 repaint 時,只需要 repaint 本身,不會影響到其他的層
          • 對于已提升為合成層中的 transform 和 opacity 效果,都只是幾何變換,透明度變換等,不會觸發(fā) layout 和 paint,直接由GPU完成即可

          因此,在開發(fā)中,建議對于需要頻繁移動的元素,建議將其提升為單獨(dú)的合成層,可減少不必要的重繪,同時可以利用硬件加速,提高渲染效率。

          4.2層繪制paint

          分好層后,就需要對每個層進(jìn)行繪制了。繪制并不是一蹴而就,而是像畫畫一樣,是按順序一筆一筆畫出來的,渲染引擎也是類似。對于每一個合成層,渲染引擎的渲染過程:

          • 先繪制下面的渲染層,再繪制上面的渲染層;
          • 在繪制一個渲染層時,將一個渲染層的繪制拆分成很多小的繪制指令。

          常見的指令如下:

          • drawReact(rect, paint):使用paint畫一個矩形rect
          • drawTextBlob(x,y,paint):使用paint以x、y為起始坐標(biāo)繪制文字
          • drawPaint(paint):用paint填充畫布
          • color:采用ARGB的方式

          各指令的含義可參考鏈接[7]

          打開“開發(fā)者工具”的“Layers”標(biāo)簽,任意選擇一層合成層,可查看該層detail下的詳細(xì)渲染列表paint profiler。繪制指令demo代碼如下

          <!DOCTYPE html>
          <html lang="en">
          <meta http-equiv="Content-Type" Content="text/html; Charset=UTF-8">
          <head>
              <style type="text/css">
                  *{
                      margin:0;
                      padding:0;
                  }
                  div{
                      width:200px;
                      height:100px;
                      text-align: center;
                      line-height:100px;
                  }
                  p{
                      height:40px;
                      line-height:40px;
                      font-size:20px;
                      margin-bottom30px;
                  }
                  .level-default{
                      position: absolute;
                      background#f5cec7;
                      top60px;
                  }
                  .level1{
                      background#ffb284;
                      position: absolute;
                      z-index:2;
                      top160px;
                  }
                  .level2{
                      background#e79796;
                      position: absolute;
                      z-index:1;
                      top260px;
                  }
                  .composite-1.composite-2{
                      position: relative;
                      transformtranslateZ(0);
                      width:300px;
                      height:400px;
                      background#ddd;
                      margin-bottom:20px;
                  }
              
          </style>
          </head>
          <body>
              <div class="composite-1">
                  <p>合成層一</p>
                  <div class='level-default'>默認(rèn)層</div>
                  <div class='level1'>渲染層1:z-index:2</div>
                  <div class='level2'>渲染層2:z-index:1</div>
              </div>
              <div class="composite-2">
                  <p>合成層二</p>
                  <div class='level-default'>默認(rèn)層</div>
                  <div class='level1'>渲染層1:z-index:2</div>
                  <div class='level2'>渲染層2:z-index:1</div>
              </div>
          </body>
          </html>

          比如選擇composite-1的合成層,繪制列表如下:

          這里順便也可以看到一點(diǎn):渲染層2在渲染層1的后面,但由于其z-index較大(說明其渲染層層級較高),因此優(yōu)先渲染層2。

          需要說明一點(diǎn),繪制列表只是用來記錄繪制順序和繪制指令的列表,并沒有真正的繪制出頁面。

          4.3柵格化

          生成了繪制指令,就到了真正繪制頁面的時候了,真正的繪制過程不是在主線程完成的,而是在得到繪制指令后,主線程會將這些信息交給合成線程,由合成線程來完成繪制。

          合成線程是如何工作的呢?

          頁面可能很大,但用戶只能看到一部分,在這種情況下如果全部繪制,就會產(chǎn)生很大的性能開銷,因此需要優(yōu)先繪制視口(即用戶看到的區(qū)域)區(qū)域內(nèi)的元素。

          基于此原因,繪制前,合成線程會對頁面進(jìn)行分塊,然后將每個圖塊發(fā)送給柵格線程,柵格線程將圖塊轉(zhuǎn)換為位圖。合成器線程可以優(yōu)先處理不同的柵格線程,這樣就可以首先對視口(或附近)中的事物進(jìn)行柵格化。

          通常,柵格化過程都會使用 GPU 來加速生成,生成的位圖被保存在 GPU 內(nèi)存中。

          柵格化的過程:

          5、合成與顯示

          柵格化完成后,每一個圖層都對應(yīng)一張“圖片”,合成線程會將這些圖片合成為一張“圖片”。此時,頁面數(shù)據(jù)已經(jīng)完成繪制,現(xiàn)在只需要顯示給用戶即可,此時就需要顯卡和顯示器就上場了。

          顯卡分為前緩沖區(qū)和后緩沖區(qū),合成線程生成的“圖片”會被發(fā)送至后緩沖區(qū)。顯卡對圖片進(jìn)行處理完成后,系統(tǒng)就會讓后緩沖區(qū)和前緩沖區(qū)互換,這樣顯示器就總能讀到顯卡最新產(chǎn)生的數(shù)據(jù)了。

          通常顯卡和顯示器的刷新頻率是一致的,都會60次/秒,但對于一些復(fù)雜的場景,顯卡處理速度比較慢,顯卡的刷新頻率就會低于顯示器,此時頁面就會出現(xiàn)卡頓現(xiàn)象。

          因此在開發(fā)中,我們需要盡量保證一幀畫面的處理總時長(以上的所有步驟)不超過1/60s = 16.7ms,這樣畫面才不會出現(xiàn)卡頓現(xiàn)象。不過量化地衡量渲染時間比較困難,但基于以上分析的渲染過程,我們就可以從渲染的各個步驟著手優(yōu)化渲染流程,提高渲染效率。

          6、相關(guān)拓展

          6.1 CSS如何影響DOM的構(gòu)建

          JavaScript腳本由于可能會修改DOM,因此會阻塞DOM的構(gòu)建,這一點(diǎn)我們都知道;而CSS并不會操作或者改變DOM,因此通常我們認(rèn)為CSS不會影響DOM的構(gòu)建,只會影響后續(xù)的布局、繪制等過程,即會影響DOM的渲染。但其實(shí)CSS可以通過JavaScript來阻塞DOM的構(gòu)建。

          因?yàn)镴avaScript是可以改變樣式的,也就是具有修改CSS規(guī)則樹的能力,而JavaScript腳本里是否有改變樣式的操作,這一點(diǎn)在執(zhí)行JavaScript之前是不可知的。因此,為保證JavaScript腳本的正確執(zhí)行,在執(zhí)行JavaScript之前,CSS規(guī)則樹必須要先準(zhǔn)備好(不然萬一有修改CSS的操作呢)。

          也就是說,若在構(gòu)建DOM的中途存在阻塞DOM構(gòu)建的JavaScript腳本,而此頁面中還包含了外部 CSS 文件的引用,那么此時就需要等目前的CSS規(guī)則樹(基于目前生成完的部分DOM樹)構(gòu)建完畢后,再開始JavaScript腳本的執(zhí)行,等一切結(jié)束了,再繼續(xù)DOM的構(gòu)建。

          整個流程如圖:(其中CSSOM表示CSS規(guī)則樹)

          demo代碼如下:

          <!DOCTYPE html>
          <html lang="en">
          <meta http-equiv="Content-Type" Content="text/html; Charset=UTF-8">
          <head>     
              <style type="text/css">
                  h4{
                      font-size:18px;
                      font-weight:none;
                  }
              
          </style>
              <link rel="stylesheet" type="text/css" href="https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/static/protocol/https/soutu/css/soutu_new2_ae491b7.css">
          </head>
          <body>
              <button id="btn">清空dom</button>
              <div>我是div</div>
              <!-- !!!script阻塞div的構(gòu)建 -->
              <script>
                      console.log('遇到內(nèi)聯(lián)script啦')
              
          </script>

              <div>我是div</div>
              <div>我是div</div>
              <div>我是div</div>
              <div>我是div</div>
          </body>
          <script type="text/javascript">
              let btn = document.getElementById('btn')
              let body = document.body
              btn.addEventListener("click"function(e){
                      body.innerHTML = ''
              }, true);
          </script>
          </html>

          將控制臺Network中的網(wǎng)絡(luò)調(diào)為Slow 3G,點(diǎn)擊按鈕清空dom后,刷新頁面觀察Element中DOM元素出現(xiàn)的時機(jī)。

          • 當(dāng)不存在script時,所有div全部很快出現(xiàn)
          • 存在script時,script后的div元素要等一會(css加載完成)才會出現(xiàn)

          說明CSS可以通過JavaScript來阻塞DOM的構(gòu)建。

          6.2重排、重繪、合成

          • 重排會改變元素的幾何位置,需要更新完整的渲染流水線,所以開銷也是最大的
          • 重繪只是修改元素的顏色等非位置屬性,所以省去了布局和分層階段,開銷比重排小
          • 合成只會由已提升會合成層的transform或opacity觸發(fā),只涉及幾何變換或透明度變換, 會跳過前面的流程,直接進(jìn)入合成階段,開銷最小。(transform或opacity若未提升為合成層,則依然會觸發(fā)paint

          另外在合成小節(jié)提到,生成繪制指令之后的分開、柵格化等工作是在合成線程中進(jìn)行,這也就意味著在執(zhí)行合成操作時,是不會影響到主線程執(zhí)行的,這也是合成動畫性能好的原因之一。也就揭示了為什么經(jīng)常主線程卡住了,但是 CSS 動畫依然能執(zhí)行的原因。

          6.3層爆炸

          前面提到overlap會導(dǎo)致生成隱式合成層,極端情況下就可能會產(chǎn)生大量的不在預(yù)期內(nèi)的額外合成層,導(dǎo)致層爆炸。demo[8]

          因此,在開發(fā)過程中,建議:

          • 為防止層爆炸,在提升為合成層的元素上,建議加上z-index,防止overlap引起的層提升。
          • 不要創(chuàng)建太多層,因?yàn)槊繉佣夹枰獌?nèi)存和管理開銷;不要在不分析的情況下提升元素。

          7、參考

          • 極客時間專欄-瀏覽器工作原理與實(shí)踐[9]
          • 無線性能優(yōu)化:composite[10]
          • Inside look at modern web browser (part 3)[11]
          • 詳談層合成[12]

          參考資料

          [1]

          極客時間專欄-瀏覽器工作原理與實(shí)踐: https://time.geekbang.org/column/intro/216

          [2]

          Vue.js的HTMLParser實(shí)現(xiàn): https://github.com/vuejs/vue/blob/dev/src/compiler/parser/html-parser.js

          [3]

          鏈接: https://www.cnblogs.com/xiaohuochai/p/5848335.html

          [4]

          few talks from BlinkOn Conference: https://www.youtube.com/watch?v=Y5Xa4H2wtVA

          [5]

          層疊上下文-MDN: https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context

          [6]

          鏈接: https://fed.taobao.org/blog/taofed/do71ct/performance-composite/

          [7]

          鏈接: https://api.flutter.dev/flutter/dart-ui/Canvas/restore.html

          [8]

          demo: http://fouber.github.io/test/layer/?size=20

          [9]

          極客時間專欄-瀏覽器工作原理與實(shí)踐: https://time.geekbang.org/column/intro/216

          [10]

          無線性能優(yōu)化:composite: https://fed.taobao.org/blog/taofed/do71ct/performance-composite/

          [11]

          Inside look at modern web browser (part 3): https://developers.google.com/web/updates/2018/09/inside-browser-part3

          [12]

          詳談層合成: http://jartto.wang/2017/09/29/expand-on-performance-composite/

          ?? 謝謝支持

          瀏覽 78
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  九九九九在线视频播放 | 91人体视频 | 日韩美女一级a黄片 | 亚洲操逼网站 | 亚洲精品成人在线视频久久 |