zoukankan      html  css  js  c++  java
  • 浏览器的渲染原理

    参考1:https://coolshell.cn/articles/9666.html#comments

    参考2:https://blog.fundebug.com/2019/01/03/understand-browser-rendering/

    思维导图

    浏览器工作大流程

     

    从上面这个图中,我们可以看到那么几个事:

    1)浏览器会解析三个东西:

    • 一个是HTML/SVG/XHTML,事实上,Webkit有三个C++的类对应这三类文档。解析这三种文件会产生一个DOM Tree。
    • CSS,解析CSS会产生CSS规则树。
    • Javascript,脚本,主要是通过DOM API和CSSOM API来操作DOM Tree和CSS Rule Tree.

    2)解析完成后,浏览器引擎会通过DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree。注意:

    • Rendering Tree 渲染树并不等同于DOM树,因为一些像Header或display:none的东西就没必要放在渲染树中了。
    • CSS 的 Rule Tree主要是为了完成匹配并把CSS Rule附加上Rendering Tree上的每个Element。也就是DOM结点。也就是所谓的Frame。
    • 然后,计算每个Frame(也就是每个Element)的位置,这又叫layout和reflow过程。

    3)最后通过调用操作系统Native GUI的API绘制。

    一、构建DOM

    HTML的DOM Tree解析如下:

    <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会解析成这样:

    下面是另一个有SVG标签的情况。

    二、构建CSSOM

    CSS的解析大概是下面这个样子(下面主要说的是Gecko也就是Firefox的玩法),假设我们有下面的HTML文档:

    <doc>
    <title>A few quotes</title>
    <para class="emph">
      Franklin said that <quote>"A penny saved is a penny earned."</quote>
    </para>
    <para>
      FDR said <quote>"We have nothing to fear but <span class="emph">fear itself.</span>"</quote>
    </para>
    </doc>

    于是DOM Tree是这个样子:

    然后我们的CSS文档是这样的:

    /* rule 1 */ doc { display: block; text-indent: 1em; }
    /* rule 2 */ title { display: block; font-size: 3em; }
    /* rule 3 */ para { display: block; }
    /* rule 4 */ [class="emph"] { font-style: italic; }

    于是我们的CSS Rule Tree会是这个样子:

     注意,图中的第4条规则出现了两次,一次是独立的,一次是在规则3的子结点。所以,我们可以知道,建立CSS Rule Tree是需要比照着DOM Tree来的。CSS匹配DOM Tree主要是从右到左解析CSS的Selector,好多人以为这个事会比较快,其实并不一定。关键还看我们的CSS的Selector怎么写了。在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。

    注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去。

    三、构建渲染树

    通过这两个树,我们可以得到一个叫Style Context Tree,也就是下面这样(把CSS Rule结点Attach到DOM Tree上):

    所以,Firefox基本上来说是通过CSS 解析 生成 CSS Rule Tree,然后,通过比对DOM生成Style Context Tree,然后Firefox通过把Style Context Tree和其Render Tree(Frame Tree)关联上,就完成了。注意:Render Tree会把一些不可见的结点去除掉。而Firefox中所谓的Frame就是一个DOM结点,不要被其名字所迷惑了

    注:Webkit不像Firefox要用两个树来干这个,Webkit也有Style对象,它直接把这个Style对象存在了相应的DOM结点上了。

    四、渲染(布局与绘制)

    渲染的流程基本上如下(黄色的四个步骤):

    1. 计算CSS样式
    2. 构建Render Tree
    3. Layout – 定位坐标和大小,是否换行,各种position, overflow, z-index属性 ……
    4. 正式开画

    注意:上图流程中有很多连接线,这表示了Javascript动态修改了DOM属性或是CSS属性会导致重新Layout,有些改变不会,就是那些指到天上的箭头,比如,修改后的CSS rule没有被匹配到,等。

    这里重要要说两个概念,一个是Reflow,另一个是Repaint。这两个不是一回事。

    • Repaint——屏幕的一部分要重画,比如某个CSS的背景色变了。但是元素的几何尺寸没有变。
    • Reflow——意味着元件的几何尺寸变了,我们需要重新验证并计算Render Tree。是Render Tree的一部分或全部发生了变化。这就是Reflow,或是Layout。(HTML使用的是flow based layout,也就是流式布局,所以,如果某元件的几何尺寸发生了变化,需要重新布局,也就叫reflow)reflow 会从<html>这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置,在reflow过程中,可能会增加一些frame,比如一个文本字符串必需被包装起来。

    问题一:渲染过程中遇到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。

    问题二:你真的了解回流和重绘吗

    我们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断重新渲染。重新渲染会重复上图中的第四步(回流)+第五步(重绘)或者只有第五个步(重绘)。

    回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

    1)常见引起回流属性和方法

    任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流,

    • 添加或者删除可见的DOM元素;
    • 元素尺寸改变——边距、填充、边框、宽度和高度
    • 内容变化,比如用户在input框中输入文字
    • 浏览器窗口尺寸改变——resize事件发生时
    • 计算 offsetWidth 和 offsetHeight 属性
    • 设置 style 属性的值

    2)常见引起重绘属性和方法

    下面例子中,触发了几次回流和重绘?

    var s = document.body.style;
    s.padding = "2px"; // 回流+重绘
    s.border = "1px solid red"; // 再一次 回流+重绘
    s.color = "blue"; // 再一次重绘
    s.backgroundColor = "#ccc"; // 再一次 重绘
    s.fontSize = "14px"; // 再一次 回流+重绘
    // 添加node,再一次 回流+重绘
    document.body.appendChild(document.createTextNode('abc!'));

    3)如何减少回流、重绘

    • 使用 transform 替代 top
    • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
    • 不要把节点的属性值放在一个循环里当成循环里的变量。(现代浏览器大多是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能会有影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流和重绘来确保返回正确的值。例如 offsetTop、clientTop、scrollTop、getComputedStyle()、width、height、getBoundingClientRect(),应避免频繁使用这些属性,他们都会强制渲染刷新队列。)
    for(let i = 0; i < 1000; i++) {
        // 获取 offsetTop 会导致回流,因为需要去获取正确的值
        console.log(document.querySelector('.test').style.offsetTop)
    }
    • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
    • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
    • CSS 选择符从右往左匹配查找,避免节点层级过多
    • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。

    问题三: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是有顺序的加载。

    问题四:为什么操作 DOM 慢

    因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。

    问题五:渲染页面时常见哪些不良现象?

    由于浏览器的渲染机制不同,在渲染页面时会出现两种常见的不良现象—-白屏问题和FOUS(无样式内容闪烁)

    FOUC:由于浏览器渲染机制(比如firefox),再CSS加载之前,先呈现了HTML,就会导致展示出无样式内容,然后样式突然呈现的现象;

    白屏:有些浏览器渲染机制(比如chrome)要先构建DOM树和CSSOM树,构建完成后再进行渲染,如果CSS部分放在HTML尾部,由于CSS未加载完成,浏览器迟迟未渲染,从而导致白屏;也可能是把js文件放在头部,脚本会阻塞后面内容的呈现,脚本会阻塞其后组件的下载,出现白屏问题。

    总结

    • 浏览器工作流程:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制。

    • CSSOM会阻塞渲染,只有当CSSOM构建完毕后才会进入下一个阶段构建渲染树。

    • 通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个script标签时,DOM构建将暂停,直至脚本完成执行。但由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS。

    • 如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,建议将 script 标签放在 body 标签底部。
  • 相关阅读:
    推荐一个学习 SharePoint 2010 的站点
    应用反射技术为Infragistics Solution设计例子程序 代码简洁而且学习的效率高
    .NET程序员掌握的.NET技术
    Jpegoptim Tool
    Stack Overflow 漫谈
    NServiceBus最流行的开源企业服务总线AND让创建企业级.NET系统更加容易
    步步为营UML建模系列四、状态图(State)
    Web中的幻灯片组件实现
    轻轻松松SOA: NServiceBus
    细说 ASP.NET Cache 及其高级用法
  • 原文地址:https://www.cnblogs.com/vickylinj/p/14179082.html
Copyright © 2011-2022 走看看