首頁 >web前端 >js教程 >總結瀏覽器渲染頁面的方法

總結瀏覽器渲染頁面的方法

零下一度
零下一度原創
2017-06-26 10:24:092883瀏覽

轉載自web fundamental

建構物件模型

瀏覽器渲染頁面前需要先建立 DOM 和 CSSOM 樹。因此,我們需要確保盡快將 HTML 和 CSS 都提供給瀏覽器。

  • 位元組 → 字元 → 標記 → 節點 → 物件模型。

  • HTML 標記轉換成文檔物件模型 (DOM);CSS 標記轉換成 CSS 物件模型 (CSSOM)。 DOM 和 CSSOM 是獨立的資料結構。

  • Chrome DevTools Timeline可以捕捉和檢查 DOM 和 CSSOM 的建置和處理開銷。

文檔物件模型 (DOM)

<html>
  <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"><title>Critical Path</title>
  </head>
  <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div>
  </body></html>

一個包含一些文字和一幅圖片的普通 HTML 頁面,瀏覽器如何處理此頁面?

HTML解析器輸出的樹是由DOM元素和屬性節點組成的,它是HTML文件的物件化描述,也是HTML元素與外界(如Javascript)的介面。 DOM與標籤有著幾乎一一對應的關係。

  

  1. 轉換: 瀏覽器從磁碟或網路讀取 HTML 的原始位元組,並根據檔案的指定編碼(如 UTF-8)將它們轉換成各個字元。

  2. Tokenizing 瀏覽器將字串轉換成 W3C HTML5 標準規定的各種tokens,例如,「」、「

    」,以及其他尖括號內的字串。每個token都有特殊意義和一組規則。
  3. 詞法分析: 發出的標記轉換成定義其屬性和規則的「物件」。

  4. DOM 構建: 最後,由於HTML 標記定義不同標記之間的關係(一些標記包含在其他標記內),創建的對象鏈接在一個樹數據結構內,此結構也會捕獲原始標記中定義的父項-子項關係:HTML 物件是 body 物件的父項,bodyparagraph物件的父項,依此類推。

整個流程最終輸出是頁面的文檔物件模型 (DOM),瀏覽器對頁面進行的所有進一步處理都會用到它。

瀏覽器每次處理 HTML 標記時,都會完成以上所有步驟:將位元組轉換成字符,確定tokens,將tokens轉換成節點,然後建立 DOM 樹。這整個流程可能需要一些時間才能完成,有大量 HTML 需要處理時更是如此。

 

如果您打開 Chrome DevTools 並在頁面載入時記錄時間線,您就可以看到執行該步驟實際花費的時間。在上例中,將一堆 HTML 位元組轉換成 DOM 樹大約需要 5 毫秒。對於較大的頁面,此過程所需的時間可能會顯著增加。創建流暢動畫時,如果瀏覽器需要處理大量 HTML,這很容易成為瓶頸。

DOM 樹捕捉文件標記的屬性和關係,但並未告訴我們元素在渲染後呈現的外觀。那是 CSSOM 的責任。

CSS 物件模型 (CSSOM)

在瀏覽器建立這個簡單頁面的 DOM 過程中,在文件的 head 中遇到了一個 link 標記,該標記引用一個外部 CSS 樣式表:style.css。由於預見到需要利用該資源來渲染頁面,它會立即發出對該資源的請求,並返回以下內容:

body { font-size: 16px }p { font-weight: bold }span { color: red }p span { display: none }img { float: right }

我們本可以直接在HTML 標記內聲明樣式(內聯),但讓CSS 獨立於HTML 有利於我們將內容和設計作為獨立關注點進行處理:設計人員負責處理CSS,開發者側重於HTML,等等。

與處理 HTML 時一樣,我們需要將收到的 CSS 規則轉換成某種瀏覽器能夠理解和處理的東西。因此,我們會重複HTML 過程,不過是為CSS 而不是HTML:

 

CSS 位元組轉換成字符,接著轉換成tokens和節點,最後連結到一個稱為「CSS 物件模型」(CSSOM) 的樹狀結構:

 

CSSOM 為何有樹狀結構?為頁面上的任何節點物件計算最後一組樣式時,瀏覽器都會先從適用於該節點的最通用規則開始(例如,如果該節點是body 元素的子元素,則套用所有body 樣式),然後透過應用更具體的規則以遞歸方式最佳化計算的樣式。

以上面的 CSSOM 樹為例進行更具體的闡述。任何置於 body 元素內span 標記中的文字都將具有 16 像素字號,並且顏色為紅色 。 font-size 指令從 body 向下級層疊至 span。不過,如果某個 span 標記是某個段落 (p) 標記的子項,則其內容將不會顯示。

Also, note that the above tree is not the complete CSSOM tree and only shows the styles we decided to override in our stylesheet.每個瀏覽器都提供一組預設樣式(也稱為「User Agent 樣式」),即我們的樣式只是override這些預設樣式。

要了解CSS 處理所需的時間,可以在DevTools 中記錄時間軸並尋找“Recalculate Style”事件:unlike DOM parsing, the timeline doesn't show a separate "Parse CSS" entry, and instead cap CSStures parsing and cap CSStures parsing and cap CSStures parsing and cap CSStures parsing and instead tree construction, plus the recursive calculation of computed styles under this one event.

 

我們的小樣式表需要大約0.6 毫秒的處理時間,影響頁面上的8 個元素— 雖然,但同樣會產生開銷。不過,這 8 個元素從何而來呢?將 DOM 與 CSSOM 關聯在一起的是渲染樹。

渲染樹建置、佈局及繪製

CSSOM 樹和 DOM 樹合併成渲染樹,然後用於計算每個可見元素的佈局,並輸出給繪製流程,將像素渲染到螢幕上。優化上述每個步驟對實現最佳渲染效能至關重要。

瀏覽器根據 HTML 和 CSS 輸入建立了 DOM 樹和 CSSOM 樹。 不過,它們是彼此完全獨立的物件,分別capture文檔不同方面的資訊:一個描述內容,另一個則是描述需要對文檔應用的樣式規則。我們該如何將兩者合併,讓瀏覽器在螢幕上渲染像素呢?

  • DOM 樹與 CSSOM 樹合併後形成渲染樹,它只包含渲染網頁所需的節點。遍歷每個DOM樹中的node節點,在CSSOM規則樹中尋找目前節點的樣式,產生渲染樹。

  • 佈局計算每個物件的精確位置和大小。

  • 最後一步是繪製,使用最終渲染樹將像素渲染到螢幕上。

 

第一步是讓瀏覽器將DOM 和CSSOM 合併成一個“渲染樹”,網羅網頁上所有可見的 DOM 內容,以及每個節點的所有內容。 。

 

為建立渲染樹,瀏覽器大體上完成了下列工作:

  1. 從 DOM 樹的根節點開始遍歷每個可見節點。

  • 某些節點不可見(例如腳本標記、元標記等),因為它們不會體現在渲染輸出中,所以會被忽略。

  • 某些節點透過 CSS 隱藏,因此在渲染樹中也會被忽略。例如 span 節點上設定了「display: none」屬性,所以也不會出現在渲染樹中。

  • 遍历每个可见节点,为其找到适配的 CSSOM 规则并应用它们。从选择器的右边往左边开始匹配,也就是从CSSOM树的子节点开始往父节点匹配。

  • Emit visible nodes with content and their computed styles.

  • 注: visibility: hidden 与 display: none 是不一样的。前者隐藏元素,但元素仍占据着布局空间(即将其渲染成一个空框),而后者 (display: none) 将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分。

    最终输出的渲染同时包含了屏幕上的所有可见内容及其样式信息。有了渲染树,我们就可以进入布局阶段。

    到目前为止,我们计算了哪些节点应该是可见的以及它们的计算样式,但我们尚未计算它们在设备视口内的确切位置和大小---这就是“布局”阶段,也称为“reflow”。

    为弄清每个对象在网页上的确切大小和位置,浏览器从渲染树的根节点开始进行遍历。让我们考虑一个简单的实例:

    <html>
      <head><meta name="viewport" content="width=device-width,initial-scale=1"><title>Critial Path: Hello world!</title>
      </head>
      <body><div style="width: 50%">  <div style="width: 50%">Hello world!</div></div>
      </body></html>

    以上網頁的正文包含兩個巢狀div:第一個(父親)div 將節點的顯示尺寸設定為視窗寬度的50%,父div 包含的第二個div寬度為其父項的50%,即視口寬度的25%。

     

     

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

    最後,既然我們知道了哪些節點可見、它們的computed styles以及幾何信息,我們終於可以將這些信息傳遞給最後一個階段:將渲染樹中的每個節點轉換成屏幕上的實際像素。這一步通常稱為"painting" or "rasterizing."。

    Chrome DevTools 可以幫助我們對上述所有三個階段的耗時進行深入的了解。讓我們來看看最初「hello world」範例的版面配置階段:

     

    The "Layout" event captures the render tree construction, position, and size calculation in the Timeline.

    . Paint Setup" and "Paint" events, which convert the render tree to pixels on the screen.

    執行渲染樹建置、佈局和繪製所需的時間將取決於文件​​大小、應用的樣式,以及運行文件的裝置:文件越大,瀏覽器需要完成的工作就越多;樣式越複雜,繪製需要的時間就越長(例如,單色的繪製開銷“較小”,而陰影的計算和渲染開銷則要“大得多”)。

    下面簡要概述了瀏覽器完成的步驟:

    1. 處理 HTML 標記並建立 DOM 樹。

    2. 處理 CSS 標記並建立 CSSOM 樹。

    3. 將 DOM 與 CSSOM 合併成一個渲染樹。

    4. 根據渲染樹來佈局,以計算每個節點的幾何資訊。

    5. 將各個節點繪製到螢幕上。

    如果 DOM 或 CSSOM 被修改,需要再執行一遍以上所有步驟,以確定哪些像素需要在螢幕上重新渲染。

    Optimizing the critical rendering path is the process of minimizing the total amount of time spent performing steps 1 through 5 in the above sequence. Doing so renders content to the screen as quickly as possible and also reduces the amount of time between screen updates after the initial render; that is, achieve higher refresh rates for interactive content.

    阻塞渲染的CSS

    預設情況下,CSS 被視為阻塞的資源(html) ,這表示瀏覽器將不會渲染任何已處理的內容,直至CSSOM 建置完畢請務必精簡CSS,盡快提供它,並利用媒體類型和查詢來解除對渲染的阻塞,以縮短首屏的時間。 在渲染樹建置中,要求同時具有 DOM 和 CSSOM 才能建立渲染樹。這會對效能造成嚴重影響:HTML

    CSS 都是阻塞渲染的資源。  HTML 顯然是必要的,因為如果沒有 DOM,就沒有可渲染的內容,但 CSS 的必要性可能就不太明顯。如果在 CSS 不阻塞渲染的情況下嘗試渲染一個普通網頁會怎麼樣?

    預設情況下,CSS 被視為阻塞渲染的資源。
    • 我們可以透過媒體類型和媒體查詢將一些 CSS 資源標記為不阻塞渲染。
    • 瀏覽器會下載所有 CSS 資源,無論阻塞或不阻塞。
    • 沒有 CSS 的網頁其實無法使用。所以瀏覽器將會阻塞渲染,直到 DOM 和 CSSOM 全準備就緒。

    CSS

    是阻塞渲染的資源。需要將它儘早、盡快地下載到客戶端,以便縮短首次渲染的時間。 如果有一些 CSS 樣式只在特定條件下(例如顯示網頁或將網頁投影到大型顯示器上時)使用,又該如何?如果這些資源不阻塞渲染,該有多好。

    可以透過 CSS「媒體類型」和「媒體查詢」來解決這類情況:



    媒体查询由媒体类型以及零个或多个检查特定媒体特征状况的表达式组成。例如,第一个样式表声明未提供任何媒体类型或查询,因此它适用于所有情况。也就是说它始终会阻塞渲染。第二个样式表则不然,它只在打印内容时适用---或许您想重新安排布局、更改字体等等,因此在网页首次加载时,该样式表不需要阻塞渲染。最后一个样式表声明提供了由浏览器执行的“媒体查询”:符合条件时,样式表会生效,浏览器将阻塞渲染,直至样式表下载并处理完毕。

    通过使用媒体查询,我们可以根据特定用例(比如显示或打印),也可以根据动态情况(比如屏幕方向变化、尺寸调整事件等)定制外观。声明样式表时,请密切注意媒体类型和查询,因为它们将严重影响关键渲染路径的性能。

    让我们考虑下面这些实例:





    • 第一个声明阻塞渲染,适用于所有情况。

    • 第二个声明同样阻塞渲染:“all”是默认类型,和第一个声明实际上是等效的。

    • 第三个声明具有动态媒体查询,将在网页加载时计算。根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。

    • 最后一个声明只在打印网页时应用,因此网页在浏览器中加载时,不会阻塞渲染。

    最后,“阻塞渲染”仅是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪。无论媒寻是否命中,浏览器都会下载上述所有的CSS样式表,只不过不阻塞渲染的资源对当前媒体不生效罢了。

    使用 JavaScript 添加交互

    JavaScript 允许我们修改网页的方方面面:内容、样式以及它如何响应用户交互。不过,JavaScript 也会阻止 DOM 构建和延缓网页渲染。为了实现最佳性能,可以让 JavaScript 异步执行,并去除关键渲染路径中任何不必要的 JavaScript。

    • JavaScript 可以查询和修改 DOM 与 CSSOM。

    • JavaScript的 执行会阻止 CSSOM的构建,所以和CSSOM的构建是互斥的。

    • JavaScript blocks DOM construction unless explicitly declared as async.

    JavaScript 是一种运行在浏览器中的动态语言,它允许对网页行为的几乎每一个方面进行修改:可以通过在 DOM 树中添加和移除元素来修改内容;可以修改每个元素的 CSSOM 属性;可以处理用户输入等等。为进行说明,让我们用一个简单的内联脚本对之前的“Hello World”示例进行扩展:

    <html>
      <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"><title>Critical Path: Script</title><style> body { font-size: 16px };p { font-weight: bold };
        span { color: red };p span { display: none };
        img { float: right }</style>
      </head>
      <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script>  var span = document.getElementsByTagName('span')[0];
          span.textContent = 'interactive'; // change DOM text content      span.style.display = 'inline';  // change CSSOM property  // create a new element, style it, and append it to the DOM  var loadTime = document.createElement('div');
          loadTime.textContent = 'You loaded this page on: ' + new Date();
          loadTime.style.color = 'blue';
          document.body.appendChild(loadTime);</script>
      </body></html>
    • JavaScript 允许我们进入 DOM 并获取对隐藏的 span 节点的引用 -- 该节点可能未出现在渲染树中,却仍然存在于 DOM 内。然后,在获得引用后,就可以更改其文本,并将 display 样式属性从“none”替换为“inline”。现在,页面显示“Hello interactive students!”。

    • JavaScript 还允许我们在 DOM 中创建、样式化、追加和移除新元素。从技术上讲,整个页面可以是一个大的 JavaScript 文件,此文件逐一创建元素并对其进行样式化。但是在实践中,使用 HTML 和 CSS 要简单得多。

    尽管 JavaScript 为我们带来了许多功能,不过也在页面渲染方式和时间方面施加了更多限制。

    首先,请注意上例中的内联脚本靠近网页底部。为什么呢?如果我们将脚本移至 span元素前面,就会脚本运行失败,并提示在文档中找不到对任何span 元素的引用 -- 即 getElementsByTagName(‘span') 会返回 null。这透露出一个重要事实:脚本在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标记时,它会暂停构建 DOM,将控制权移交给 JavaScript 引擎;等 JavaScript 引擎运行完毕,浏览器会从中断的地方恢复 DOM 构建。

    换言之,我们的脚本块在运行时找不到网页中任何靠后的元素,因为它们尚未被处理!或者说:执行内联脚本会阻止 DOM 构建,也就延缓了首次渲染。

    在网页中引入脚本的另一个微妙事实是,它们不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性。实际上,示例中就是这么做的:将 span 元素的 display 属性从 none 更改为 inline。最终结果如何?我们现在遇到了race condition(资源竞争)。

    如果浏览器尚未完成 CSSOM 的下载和构建,而却想在此时运行脚本,会怎样?答案很简单,对性能不利:浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。

    简言之,JavaScript 在 DOM、CSSOM 和 JavaScript 执行之间引入了大量新的依赖关系,从而可能导致浏览器在处理以及在屏幕上渲染网页时出现大幅延迟:

    • 脚本在文档中的位置很重要。

    • 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。

    • JavaScript 可以查询和修改 DOM 与 CSSOM。

    • JavaScript 执行将暂停,直至 CSSOM 就绪。即CSSDOM构建的优先级更高。

    “优化关键渲染路径”在很大程度上是指了解和优化 HTML、CSS 和 JavaScript 之间的依赖关系谱。

    解析器阻塞与异步 JavaScript

    默认情况下,JavaScript 执行会“阻塞解析器”:当浏览器遇到文档中的脚本时,它必须暂停 DOM 构建,将控制权移交给 JavaScript 运行时,让脚本执行完毕,然后再继续构建 DOM。实际上,内联脚本始终会阻止解析器,除非编写额外代码来推迟它们的执行。

    通过 script 标签引入的脚本又怎样:

    <html>
      <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"><title>Critical Path: Script External</title>
      </head>
      <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script src="app.js?1.1.11"></script>
      </body></html>

    app.js

    var span = document.getElementsByTagName('span')[0];
    span.textContent = 'interactive'; // change DOM text contentspan.style.display = 'inline';  // change CSSOM property// create a new element, style it, and append it to the DOMvar loadTime = document.createElement('div');
    loadTime.textContent = 'You loaded this page on: ' + new Date();
    loadTime.style.color = 'blue';
    document.body.appendChild(loadTime);

    无论我们使用