简单渲染过程
url解析:
用户输入URL地址
浏览器解析URL解析出主机名
浏览器将主机名转换成服务器ip地址(浏览器先查找本地DNS缓存列表 没有的话 再向浏览器默认的DNS服务器发送查询请求 同时缓存)
浏览器将端口号从URL中解析出来
浏览器建立一条与目标Web服务器的TCP连接(三次握手)
浏览器向服务器发送一条HTTP请求报文
服务器向浏览器返回一条HTTP响应报文
关闭连接 浏览器解析文档
如果文档中有资源 重复6 7 8 动作 直至资源全部加载完毕
html解析:
- 将HTML构建成一个DOM树(DOM = Document Object Model 文档对象模型),DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
- 将CSS解析成CSS去构造CSS Rule Tree
- 根据DOM树和CSSOM来构造 Rendering Tree(渲染树)。注意:Rendering Tree 渲染树并不等同于 DOM 树,因为一些像 Header 或 display:none 的东西就没必要放在渲染树中了。
- 有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。
- 下一步操作称之为Layout,顾名思义就是计算出每个节点在屏幕中的位置 layout render tree。
- 再下一步就是绘制(Paint),即遍历render树,并使用浏览器UI后端层绘制每个节点。
Render Tree
渲染树,代表一个文档的视觉展示,浏览器通过它将文档内容绘制在浏览器窗口,展示给用户,它由按顺序展示在屏幕上的一系列矩形对象组成,这些矩形对象都带有字体,颜色和尺寸,位置等视觉样式属性。对于这些矩对象,FireFox称之为框架(frame),Webkit浏览器称之为渲染对象(render object, renderer),后文统称为渲染对象。
每一个渲染对象都代表着其对应DOM节点的CSS盒子,该盒子包含了尺寸,位置等几何信息,同时它指向一个样式对象包含其他视觉样式信息。
每一个渲染对象都对应着DOM节点,但是非视觉(隐藏,不占位)DOM元素不会插入渲染树,如<head>
元素或声明display: none;
的元素,渲染对象与DOM节点不是简单的一对一的关系,一个DOM可以对应一个渲染对象,但一个DOM元素也可能对应多个渲染对象,因为有很多元素不止包含一个CSS盒子,如当文本被折行时,会产生多个行盒,这些行会生成多个渲染对象;又如行内元素同时包含块元素和行内元素,则会创建一个匿名块级盒包含内部行内元素,此时一个DOM对应多个矩形对象(渲染对象)。
Layout
创建渲染树后,下一步就是布局(Layout),或者叫回流(reflow,relayout),这个过程就是通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸,将其安置在浏览器窗口的正确位置,而有些时候我们会在文档布局完成后对DOM进行修改,这时候可能需要重新进行布局,也可称其为回流,本质上还是一个布局的过程,每一个渲染对象都有一个布局或者回流方法,实现其布局或回流。
Paint
最后是绘制(paint)阶段或重绘(repaint)阶段,浏览器UI组件将遍历渲染树并调用渲染对象的绘制(paint)方法,将内容展现在屏幕上,也有可能在之后对DOM进行修改,需要重新绘制渲染对象,也就是重绘,绘制和重绘的关系可以参考布局和回流的关系。
Reflow (回流/重排)
当它发现了某个部分发生了变化影响了布局,渲染树需要重新计算。
原因:
- DOM操作,如增加,删除,修改或移动;
- 变更内容;
- 激活伪类;
- 访问或改变某些CSS属性(包括修改样式表或元素类名或使用JavaScript操作等方式);
- 浏览器窗口变化(滚动或尺寸变化)
如何减少reflow
- 尽可能限制reflow的影响范围。需要改变元素的样式,不要通过父级元素影响子元素。最好直接加在子元素上。
- 通过设置style属性改变结点样式的话,每设置一次都会导致一次reflow。所以最好通过设置class的方式。
- 减少不必要的DOM层级(DOM depth)。改变DOM树中的一级会导致所有层级的改变,上至根部,下至被改变节点的子节点。这导致大量时间耗费在执行reflow上面。
- 避免不必要的复杂的CSS选择器,尤其是后代选择器(descendant selectors),因为为了匹配选择器将耗费更多的CPU。
Repaint(重绘)
改变了某个元素的背景颜色,文字颜色等,不影响元素周围或内部布局的属性,将只会引起浏览器的repaint,根据元素的新属性重新绘制,使元素呈现新的外观。重绘不会带来重新布局,并不一定伴随重排; Reflow要比Repaint更花费时间,也就更影响性能。所以在写代码的时候,要尽量避免过多的Reflow。
script解析
或许是由于通常会在JavaScript脚本中改变文档DOM结构,于是浏览器以同步方式解析,加载和执行脚本,浏览器在解析文档时,当解析到<script>
标签时,会解析其中的脚本(对于外链的JavaScript文件,需要先加载该文件内容,再进行解析),然后立即执行,这整个过程都会阻塞文档解析,直到脚本执行完才会继续解析文档。就是说由于脚本是同步加载和执行的,它会阻塞文档解析,这也解释了为什么现在通常建议将<script>
标签放在</body>
标签前面,而不是放在<head>
标签里。现在HTML5提供defer和async两个属性支持延迟和异步加载JavaScript文件
defer和async
当然我们可以通过设置defer
和async
属性来异步加载不太重要的脚本
这两个属性都告诉浏览器,他可能会在后台加载脚本时继续解析HTML,然后再在加载后执行脚本,这样脚本下载不会阻止DOM构建和页面呈现。用户可以在所有脚本完成加载之前看到页面
两者的区别就是他们将在那一刻执行脚本,在这之前我们需要了解浏览器为其加载的每个网页追踪细粒度时间戳
domLoading
: 浏览器即将开始解析第一批收到的HTML文档字节domInteractive
: 表示浏览器完成对所有HTML的解析并且DOM构建完成的时间点domContentLoaded
: 表示DOM准备就绪并且没有样式表阻止JavaScript执行的时间点domComplete
: 所有处理完成,并且网页上的所有资源都已经下载完毕loadEvent
: 作为每个网页加载的最后一步,浏览器会触发onload
事件,以便触发额外的应用逻辑
defer
的执行将在domInteractive
完成之后,domContentLoaded
之前开始,他保证脚本将按照他们在HTML中出现的顺序执行,并且不会阻塞解析器
async
脚本在完成下载之后和窗口load
事件之前的某一个时间点执行,这意味着异步脚本可能不按他们在HTML中出现的顺序执行,这意味着他们可能会阻止DOM构建
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 事件。
defer 不会改变 script 中代码的执行顺序,示例代码会按照 1、2、3 的顺序执行。所以,defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。
async
<script src="app.js" async></script>
<script src="ad.js" async></script>
<script src="statistics.js" async></script>
复制代码
async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。
从上一段也能推出,多个 async-script 的执行顺序是不确定的。值得注意的是,向 document 动态添加 script 标签时,async 属性默认是 true
CSS和JS阻塞规则
- CSS 不会阻塞 DOM 的解析,但会阻塞 DOM 渲染。
- JS 阻塞 DOM 解析,但浏览器会"偷看"DOM,预先下载相关资源。
- 浏览器遇到
<script>
且没有defer或async属性的 标签时,会触发页面渲染,因而如果前面CSS资源尚未加载完毕时,浏览器会等待它加载完毕在执行脚本。
渲染优化
- HTML文档结构层次尽量少,最好不深于六层;
- 脚本尽量后放,放在前即可;
- 少量首屏样式内联放在标签内;
- 样式结构层次尽量简单;
- 在脚本中尽量减少DOM操作,尽量缓存访问DOM的样式信息,避免过度触发回流;
- 减少通过JavaScript代码修改元素样式,尽量使用修改class名方式操作样式或动画;
- 动画尽量使用在绝对定位或固定定位的元素上;
- 隐藏在屏幕外,或在页面滚动时,尽量停止动画;
- 尽量缓存DOM查找,查找器尽量简洁;
- 涉及多域名的网站,可以开启域名预解析
参考
- https://www.cnblogs.com/CandyManPing/p/6635008.html
- http://blog.codingplayboy.com/2017/03/29/webpage_render/
作者:Jay_Gao
链接:https://juejin.im/post/5b2e352ef265da59af409832
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。