浏览器渲染

浏览器渲染:
概念:
页面渲染就是浏览器将html代码根据CSS定义的规则显示在浏览器窗口中的这个过程

为什么要了解浏览器加载、解析、渲染这个过程?
一.目的:
  • 了解浏览器如何进行加载,可以在引用外部样式文件,外部js时,将他们放到合适的位置,使浏览器以最快的速度将文件加载完毕。
  • 了解浏览器如何进行解析,可以在构建DOM结构,组织css选择器时,选择最优的写法,提高浏览器的解析速率。
  • 了解浏览器如何进行渲染,明白渲染的过程,在设置元素属性,编写js文件时,可以减少”reflow“”repaint“的消耗。
二.下面是渲染引擎在取得内容之后的基本流程
 解析html以构建dom树 -> 构建render树 -> 布局render树 -> 绘制render树
所以,浏览器会解析三个东西:
(1) HTML/SVG/XHTML,解析这三种文件会产生一个 DOM Tree。
(2) CSS,解析 CSS 会产生 CSS 规则树。
(3) Javascript脚本,主要是通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree.
当浏览器获得一个html文件时,会 “自上而下”加载,并在加载过程中进行解析渲染。
解析:
1. 浏览器会将HTML解析成一个DOM树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
2. 将CSS解析成 CSS Rule Tree 。
3. 根据DOM树和CSSOM来构造 Rendering Tree。注意:Rendering Tree 渲染树并不等同于 DOM 树,因为一些像 Header 或 display:none 的东西就没必要放在渲染树中了。

4.有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。下一步操作称之为Layout,顾名思义就是计算出每个节点在屏幕中的位置。
5.再下一步就是绘制,即遍历render树,并使用UI后端层绘制每个节点。

三 .一些专业名词
那是因为浏览器要花时间、花精力去渲染,尤其是当它发现某个部分发生了点 变化影响了布局 ,需要倒回去 重新渲染,内行称这个回退的过程叫 reflow。

repaint,中文叫重绘 。如果只是改变某个元素的背景色、文字颜色、边框颜色等等 不影响它周围或内部布局 的属性,将只会引起浏览器repaint。repaint的速度明显快于 reflow(在IE下需要换一下说法,reflow要比repaint 更缓慢)。

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

Reflow 的成本比 Repaint 的成本高得多的多。DOM Tree里的每个结点都会有 reflow 方法,一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow。在一些高性能的电脑上也许还没什么,但是如果 reflow 发生在手机上,那么这个过程是非常痛苦和耗电的。
  所以,下面这些动作有很大可能会是成本比较高的。
  • 当你增加、删除、修改 DOM 结点时,会导致 Reflow 或 Repaint。
  • 当你移动 DOM 的位置,或是搞个动画的时候。
  • 当你修改 CSS 样式的时候。
  • 当你 Resize 窗口的时候(移动端没有这个问题),或是滚动的时候。
  • 当你修改网页的默认字体时。
  注:display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发现位置变化。

reflow
基本上来说,reflow 有如下的几个原因:
  • Initial。网页初始化的时候。
  • Incremental。一些 Javascript 在操作 DOM Tree 时。
  • Resize。其些元件的尺寸变了。
  • StyleChange。如果 CSS 的属性发生变化了。
  • Dirty。几个 Incremental 的 reflow 发生在同一个 frame 的子树上。
一般来说,浏览器会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。但是有些情况浏览器是不会这么做的,比如:resize 窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行 reflow。

减少 reflow/repaint
  下面是一些 Best Practices:
   1)不要一条一条地修改 DOM 的样式。与其这样,还不如预先定义好 css 的 class,然后修改 DOM 的 className。
2)把 DOM 离线后修改。如:
  • 使用 documentFragment 对象在内存里操作 DOM。
  • 先把 DOM 给 display:none (有一次 repaint),然后你想怎么改就怎么改。比如修改 100 次,然后再把他显示出来。
  • clone 一个 DOM 结点到内存里,然后想怎么改就怎么改,改完后,和在线的那个的交换一下。
  3) 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量。不然这会导致大量地读写这个结点的属性。
  4) 尽可能的修改层级比较低的 DOM。当然,改变层级比较底的 DOM 有可能会造成大面积的 reflow,但是也可能影响范围很小。
  5) 为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。
  6) 千万不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。

四.资源外链的下载
上面介绍了html解析,渲染流程。但实际上,在解析html时,会遇到一些资源连接,此时就需要进行单独处理了
简单起见,这里将遇到的静态资源分为一下几大类(未列举所有):
  • CSS样式资源
  • JS脚本资源
  • img图片类资源
遇到外链时的处理
当遇到上述的外链时,会单独开启一个下载线程去下载资源(http1.1中是每一个资源的下载都要开启一个http请求,对应一个tcp/ip链接)
遇到CSS样式资源
CSS资源的处理有几个特点:
  • CSS下载时异步,不会阻塞浏览器构建DOM树
  • 但是会阻塞渲染,也就是在构建render时,会等到css下载解析完毕后才进行 (这点与浏览器优化有关,防止css规则不断改变,避免了重复的构建)
  • 有例外,media query声明的CSS是不会阻塞渲染的
遇到JS脚本资源
JS脚本资源的处理有几个特点:
  • 阻塞浏览器的解析,也就是说发现一个外链脚本时,需等待脚本下载完成并执行后才会继续解析HTML
  • 浏览器的优化,一般现代浏览器有优化,在脚本阻塞时,也会继续下载其它资源(当然有并发上限), 但是虽然脚本可以并行下载,解析过程仍然是阻塞的,也就是说必须这个脚本执行完毕后才会接下来的解析, 并行下载只是一种优化而已
  • defer与async,普通的脚本是会阻塞浏览器解析的,但是可以加上defer或async属性,这样脚本就变成异步了, 可以等到解析完毕后再执行
注意,defer和async是有区别的, defer是延迟执行,而async是异步执行。
简单的说(不展开):
  • async是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在onload前, 但不确定在DOMContentLoaded事件的前或后
  • defer是延迟执行,在浏览器看起来的效果像是将脚本放在了body后面一样 (虽然按规范应该是在DOMContentLoaded事件前,但实际上不同浏览器的优化效果不一样, 也有可能在它后面)
遇到img图片类资源
遇到图片等资源时,直接就是异步下载,不会阻塞解析, 下载完毕后直接用图片替换原有src的地方
loaded和domcontentloaded
简单的对比:
  • DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片 (譬如如果有async加载的脚本就不一定完成)
  • load 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了


五.编写CSS时应该注意:
CSS选择符是从右到左进行匹配的。从右到左!所以,#nav li 我们以为这是一条很简单的规则,秒秒钟就能匹配到想要的元素,但是,但是,但是,是从右往左匹配啊,所以,会去找所有的li,然后再去确定它的父元素是不是#nav。,因此,写css的时候需要注意:
  1. dom深度尽量浅。
  2. 减少inline javascript、css的数量。
  3. 使用现代合法的css属性。
  4. 不要为id选择器指定类名或是标签,因为id可以唯一确定一个元素。
  5. 避免后代选择符,尽量使用子选择符。原因:子元素匹配符的概率要大于后代元素匹配符。后代选择符;#tp p{} 子选择符:#tp>p{}
  6. 避免使用通配符,举一个例子,.mod .hd *{font-size:14px;} 根据匹配顺序,将首先匹配通配符,也就是说先匹配出通配符,然后匹配.hd(就是要对dom树上的所有节点进行遍历他的父级元素),然后匹配.mod,这样的性能耗费可想而知.

(前端小白,如有错误,欢迎指正~~)