Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浏览器渲染机制 #22

Open
Genluo opened this issue Aug 31, 2019 · 1 comment
Open

浏览器渲染机制 #22

Genluo opened this issue Aug 31, 2019 · 1 comment

Comments

@Genluo
Copy link
Owner

Genluo commented Aug 31, 2019

当我们了解浏览器的渲染过程、渲染原理、其实就是掌握了核心,根据优化原则,可以实现出无数种具体的优化方案,各种功能预编译、预加载、资源合并、按需加载方案都是针对浏览器渲染习惯的优化。

输入一个Url经过哪些步骤

面试经常问道的问题,这个问题可以回答很有深度,涉及的面比较广,但是也可以回答的比较简单,如下:

  • 解析出主机名
  • DNS查询(UDP协议传输报文)
  • TCP连接
  • HTTP请求
  • 服务器进行响应
  • 客户端进行渲染

浏览器渲染页面流程

当浏览器拿到html时候,开始解析html,构建DOM,从上往下,在这个过程中遇见css,如果是内联就开启另一个线程来构建CSSOM(紫色的css),如果是外链的话就会就会开启另一线程进行下载,下载完成后进行构建CSSOM,如果在这个过程中遇见script并且async和defer都为false,这时候就会停止解析html,同时,如果目前还有未下载完成的CSS资源或者现有的css还没有构建完CSSOM,js就会进行等待,直到下载完目前的CSS资源,构建完当前的CSSOM,当CSSOM构建完成之后,通过目前的DOM tree和CSSOMs进行一次计算,也就是construct->rendering tree的过程,(渲染树只包含可见的节点),这个过程中,如果是第一次进行构建肯定会触发一次reflow/layout,然后浏览器启动另一个线程进行pain -> Graphics API,如果不是第一次构建,则会判断当前新的计算需不需要触发reflow,更新rendering tree,当构建完成,这时候就可以去执行前面阻塞js,在执行js的过程中,js将通过DOM API和CSSOM API进行更改DOM 和CSSOM,按照正常流程就是更改一次进行contruct一次,然后update rendering tree,但是浏览器做了优化,具体的优化在reflow和repaint的介绍中,到js执行完毕,就会继续parse html 构建DOM,重复执行上述过程,这中间的几点总结如下:

  • CSS 不会阻塞 DOM 的解析,但会阻塞 DOM 渲染。
  • JS 阻塞 DOM 解析,但聪明的浏览器会"偷看"DOM,预先下载相关资源。
  • 浏览器遇到 <script>且没有deferasync属性的标签时,会触发页面渲染(构建出rendering tree),因而如果前面CSS资源尚未加载完毕时,浏览器会等待它加载完毕再执行脚本。所以这时候CSSOM会阻塞javascript的执行,javascript执行会阻塞DOM的构建

常见的优化手段

  • 为什么js放到body最后、css放到head中比较好?

    因为script会阻塞DOM的解析,但是CSS不会阻塞DOM的解析

  • 同时将js 和 css放置在一起的时候,js在前面,性能好点为什么

    因为遇见script标签的时候,需要等待当期那CSSOM树构建完成,这样的话有可能CSS将会阻塞整个页面的渲染,所以在遇见script和style的时候,优先执行script,除非特殊需要

reflow(回流)和repaint(重绘)两个过程

上面页面渲染的过程中,js的执行,或者相关逻辑将会通过不同的方式更改DOM或者CSSOM,在这些过程中浏览器将会根据计算来确定需要进行哪些操作,最为典型的就是reflow和repaint,也是我们能够通过此处进行优化页面的两点。首先谈下自己的理解:不论是正在执行的js或者是css伪类激活等等都是更改CSSOM和DOM两者,然后通过DOM或者CSSOM进行构造形成rendering tree进行渲染,页面展示出来,如果是第一次进行构造rendering tree,将会触发layout进行计算元素位置和几何结构,如果已经存在rendering tree将会根据CSSOM或者DOM的变化,判断是不是需要进行reflow过程(计算节点的位置或者几何结构-部分或者全部),当成功构建rendering tree或者更新rendering tree ,如果第一次将会paint,如果不是第一次将会触发repaint过程,然后将页面展示在浏览器上。

reflow/layout过程

重新计算文档中元素的位置和几何结构的过程(根据实际分为:部分或者全部)

可以通过下面这几种方法进行触发

  • 元素属性的变化
    • 盒模型相关的属性: widthheightmargindisplayborder,etc
    • 定位属性及浮动相关的属性: top,position,floatetc
    • 改变节点内部文字结构也会触发回流: text-align, overflow, font-size, line-height, vertival-align,etc
  • 调整窗口大小
  • 样式表变动
  • 元素内容变化,尤其是输入控件
  • dom操作
  • css伪类激活
  • 计算元素的offsetWidthoffsetHeightclientWidthclientHeightwidthheightscrollTopscrollHeight

浏览器中的执行过程

  • Recalculate Style:浏览器计算改变过后的样式
  • Layout:这个过程就是我们说得reflow回流过程
  • Update Layer Tree:更新Layer Tree
  • Paint:图层的绘制过程
  • Composite Layers:合并多个图层

repaint过程

根据rendering tree渲染生成页面

触发的方法如下:

  • 页面中的元素更新样式风格相关的属性时就会触发重绘,如backgroundcolorcursorvisibility,etc

浏览器执行过程如下:

  • Recalculate Style:浏览器计算改变过后的样式
  • Update Layer Tree:更新Layer Tree
  • Paint:图层的绘制过程
  • Composite Layers:合并多个图层

浏览器对于reflow的优化

浏览器为了防止我们犯二把多次reflow操作放在循环中而引发浏览器假死,做了一个聪明的小动作。它会收集reflow操作到缓存队列中直到一定的规模或者过了特定的时间,再一次性地flush队列,反馈到render tree中,这样就将多次的reflow操作减少为少量的reflow。如果我们想要在一次reflow过后就获取元素变动过后的值呢?这个时候浏览器为了获取真实的值就不得不立即flush缓存的队列。这些值包括:

  • offsetTop/Left/Width/Height
  • scrollTop/Left/Width/Height
  • clientTop/Left/Width/Height
  • getComputedStyle(), or currentStyle in IE

例如可通过下面两种方式进行测试

    // 浏览器做优化,将多次reflow减少为少量的reflow
    document.addEventListener('DOMContentLoaded', function () {
        var date = new Date();
        for (var i = 0; i < 70000; i++) {
            var tmpNode = document.createElement("div");
            tmpNode.innerHTML = "test" + i;
            document.body.appendChild(tmpNode);
        }
        console.log(new Date() - date);
    }); 

// 因为js的操作,浏览器需要每次进行reflow,页面性能差,造成假死
document.addEventListener('DOMContentLoaded', function () {
    var date = new Date();
    for (var i = 0; i < 70000; i++) {
        var tmpNode = document.createElement("div");
        tmpNode.innerHTML = "test" + i;
        document.body.offsetHeight; // 获取body的真实值
        document.body.appendChild(tmpNode);
    }
    console.log("speed time", new Date() - date);
});

避免reflow和repaint带来的性能开销

css层面避免回流、重绘

  • 尽可能在DOM树的最末端改变class
  • 避免设置多层内联样式
  • 动画效果应用到position属性为absolute或fixed的元素上
  • 使用css3硬件加速,可以让transform、opacity、filters等动画效果**(不会引起回流重绘)**
    • 但是如果使用CSS3过多的话,可能出现内存占用较大,会有性能问题
    • 对于动画的其他属性,比如背景颜色还是会引起重绘的
    • 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

javascript层面上避免回流、重绘

  • 避免使用JS一个样式修改完接着改下一个样式,最好一次性更改CSS样式,或者将样式列表定义为class的名称
  • 避免频繁操作DOM,使用文档片段创建一个子树,然后再拷贝到文档中
  • 先隐藏元素,使其脱离文档流,进行修改后再显示该元素,因为display:none上的DOM操作不会引发回流和重绘
  • 避免循环读取offsetLeft等属性,在循环之前把它们存起来
  • 对于复杂动画效果,使用绝对定位让其脱离文档流,否则会引起父元素及后续元素大量的回流

defer和async

首先需要注意,这两个属性都是对具有src属性的script标签有效,对于内联script无效

  • defer

defer表示延迟执行引入的javascript,即就是dom解析到defer属性的script脚本,不会停止解析,等到整个dom解析完毕后,按照defer-script顺序执行,执行完毕后触发DOMContentLoaded事件,但是网上有一种声音说的是:由于浏览器实现标准的不同,使用defer不一定能够保证脚本一定按照顺序执行,

  • async

async表示异步执行引入的script,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,一定在 load 触发之前执行。

  • document.createElement

这也就是自己这次遇见的问题,最终发现通过这种方式创建的script默认是异步的,所以来讲,动态创建的script是不会阻塞页面的,如果想要同步进行执行可以将async设置为false,如果使用这种方法创建link标签的话,实际测试chrome是不会阻塞渲染的。

  • 上面两种js执行图解

页面渲染触发的事件

页面在渲染过程会触发很多事件,例如window.onload 等等,还有打开chrome network下面展示的DOMContentLoaded时间和load时间等等,这些到底在页面渲染的哪个时间点执行?下面就是结合浏览器和页面渲染图解来详细探讨一番

DOM API的事件

  • DOMContentLoaded

加载完页面,解析完所有标签(不包括执行CSS和JS),执行每个同步的script标签中的JS,执行完毕后然后触发。补全表单也是在这个时间点后进行补全

  • load

window.onload: 所有文件包括样式表,图片和其他资源下载完成后触发

  • readystatechange

通过document.readyState 可以读取当前页面加载状态,会有三个值:①loading:加载 - dom正在加载②interactive互动-文档已被解析,但是诸如图像,样式表和框架之类的子资源仍然在加载③complete 文档所有资源完成加载,状态表示load即将被触发

  • beforeunload

用户即将离开或者关闭窗口时候

  • unload

用户离开页面的时候

页面加载中的时间点

  • domLoading:这是整个过程的起始时间戳,浏览器即将开始解析第一批收到的 HTML 文档字节。
  • domInteractive:表示浏览器完成对所有 HTML 的解析并且 DOM 构建完成的时间点。
  • domContentLoaded:表示 DOM 准备就绪并且没有样式表阻止 JavaScript 执行的时间点,这意味着现在我们可以构建渲染树了。
  • 许多 JavaScript 框架都会等待此事件发生后,才开始执行它们自己的逻辑。因此,浏览器会捕获 EventStart 和 EventEnd 时间戳,让我们能够追踪执行所花费的时间。
  • domComplete:顾名思义,所有处理完成,并且网页上的所有资源(图像等)都已下载完毕,也就是说,加载转环已停止旋转。
  • loadEvent:作为每个网页加载的最后一步,浏览器会触发 onload 事件,以便触发额外的应用逻辑。

HTML 规范中规定了每个事件的具体条件:应在何时触发、应满足什么条件等等。对我们而言,我们将重点放在与关键渲染路径有关的几个关键里程碑上:

  • domInteractive 表示 DOM 准备就绪的时间点。

  • domContentLoaded 一般表示 DOM 和 CSSOM 均准备就绪的时间点。如果没有阻塞解析器的 JavaScript,则 DOMContentLoaded 将在 domInteractive 后立即触发。

  • domComplete 表示网页及其所有子资源都准备就绪的时间点。

参考

@Genluo
Copy link
Owner Author

Genluo commented Dec 2, 2019

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant