浏览器页面加载解析渲染机制

一:为什么要了解浏览器渲染页面和加载页面机制,主要还是性能的优化。

  • 了解浏览器如何进行加载,我们可以在引用外部样式文件,外部js时,将他们放到合适的位置,使浏览器以最快的速度将文件加载完毕。
  • 了解浏览器如何进行解析,我们可以在构建DOM结构,组织css选择器时,选择最优的写法,提高浏览器的解析速率。
  • 了解浏览器如何进行渲染,明白渲染的过程,我们在设置元素属性,编写js文件时,可以减少”重绘“”重新布局“的消耗。

二:用户访问网页都发生了什么。

  1. 用户访问网页,DNS服务器(域名解析系统)会根据用户提供的域名查找对应的IP地址,找到后,系统会向对应IP地址的网络服务器发送一个http请求。
  2. 网络服务器解析请求,并发送请求给数据库服务器。
  3. 数据库服务器将请求的资源返回给网络服务器,网络服务器解析数据,并生成html文件,放入http response中,返回给浏览器。
  4. 浏览器解析 http response。
  5. 浏览器解析 http response后,需要下载html文件,以及html文件内包含的外部引用文件,及文件内涉及的图片或者多媒体文件。(这里进入主题了也就是下面的第三大点)

1~4步骤HTTP协议的一些内容,访问服务器端可能遭遇的问题:如果网络服务器无法获取数据库服务器返回的资源文件(http response 404),或者由于并发原因暂时无法处理用户的http请求(http response 500)。

三:浏览器加载页面机制

加载,即为获取资源文件的过程,不同浏览器,以及他们的不同版本在实现这一过程时,会有不同的实现效果(资源间互相阻塞,可以用timeline来做测试)。这里先说下浏览器的5个常驻线程:

  1. 浏览器GUI渲染线程
  2. javascript引擎线程
  3. 浏览器定时器触发线程(setTimeout)
  4. 浏览器事件触发线程
  5. 浏览器http异步请求线程(.jpg 这类请求)

备注:现代浏览器存在 prefetch 优化,浏览器会另外开启线程,提前下载js、css文件,需要注意的是,预加载js并不会改变dom结构,他将这个工作留给主加载。

注意:这里也涉及到 阻塞 的现象,当js引擎线程(第二个)进行时,会挂起其他一切线程,这个时候3、4、5这三类线程也会产生不同的异步事件,由于 javascript引擎线程为单线程,所以代码都是先压到队列,采用先进先出的方式运行,事件处理函数,timer函数也会压在队列中,不断的从队头取出事件,这就叫:javascript-event-loop。简单点说应该是当在进行第二线程的时候,1,3,4,5都会挂起,比如这时候触发click事件,即使先前JS已经加载完成,click事件会压在队列里,这里也要先完成第二线程才会执行click事件。

加载顺序:

  1. 浏览器解析http response 下载html文件会”自上而下“加载,并在加载过程中进行解析渲染。“自上而下”加载时遇到图片、视频之类资源时便会进入第5个线程,这是异步请求,并不会影响html文档进行加载。
  2. 加载过程中遇到外部css文件,浏览器另外发出一个请求,来获取css文件。这里也是第5个线程,这里css解析会生成一个rule tree(规则树),这个以后会更新。
  3. 当文档加载过程中遇到js文件,html文档会挂起渲染(加载解析渲染同步)的线程,不仅要等待文档中js文件加载完毕,还要等待解析执行完毕,才可以恢复html文档的渲染线程。

原因:JS有可能会修改DOM,最为经典的document.write,这意味着,在JS执行完成前,后续所有资源的下载可能是没有必要的,这是js阻塞后续资源下载的根本原因。

办法:可以将外部引用的js文件放在前。

4、 css可能影响js的执行造成阻塞。

原因:如js里面var width = $(‘#id’).width();这里js执行前,浏览器必须保证之前的css文件已下载和解析完成(后面的不会影响),这也是css阻塞后续js的根本原因。当js文件不需要依赖css文件时,可以将js文件放在头部css的前面。

5、 预加载网页,利用空余时间来提前加载该网页的后续网页。

1
<link rel="prefetch" href="http://">

6、为js脚本添加defer属性,其不会阻塞后续DOM的的渲染。但是因为这个defer只是IE专用,所以一般用得比较少。

而我们标准的的HTML5也加入了一个异步载入javascript的属性:async,无论你对它赋什么样的值,只要它出现,它就开始异步加载js文件。

但是, async的异步加载会有一个比较严重的问题,那就是它忠实地践行着“载入后马上执行”这条军规,所以,虽然它并不阻塞页面的渲染,但是你也无法控制他执行的次序和时机。

四、浏览器解析渲染机制

1.浏览器是如何对网页进行渲染的

(1).浏览器将从服务器获取的HTML文档构建成文档对象模型DOM(Document Object Model)

(2).样式将被载入和解析,构成层叠样式表模型CSSOM(CSS Object Model)

(3).在DOM和CSSOM之上,渲染树(rendering tree)将会被创建,代表一系列将被渲染的对象(这在Webkit内核中被称为renderer或者渲染对象render object,在Gecko内核中被称为框架frame)。渲染树映射除了不可见元素(例如或者含有display:none;的标签)外的所有DOM结构。每一段文本字符串都将划分在不同的渲染对象中,每一个渲染对象都包含了它相应的DOM对象以及计算后的样式。换句话讲,渲染树是DOM的直观表示。

(4).渲染树的每个元素包含的内容都是计算过的,它被称之为布局layout.浏览器使用一种流式处理的方法,只需要一次pass绘制操作就可以布局所有的元素(tables需要多次pass绘制,pass表示像素处理和顶点处理)。

(5).最后布局完成,渲染树将转化为屏幕上的实际内容,这一步被称为绘制painting。

DOM-CSSOM-rendering tree-layout-painting

2.重绘Repaint

当页面元素样式的改变不影响元素在文档流中的位置时(例如background-color, border-color,visibility),浏览器只会将新样式赋予元素并进行重绘操作。

3.回流Reflow

当改变影响文档内容或者结构,或者元素位置时,回流操作就会被触发,一般有以下几种情况:

  1. DOM操作(对元素的增删改,顺序变化等)
  2. 内容变化,包括表单区域内的文本改变
  3. CSS属性的更改或重新计算
  4. 增删样式表内容
  5. 修改class属性
  6. 浏览器窗口变化(滚动或缩放)
  7. 伪类样式激活(:hover等)

4.浏览器如何优化渲染

浏览器本身会尽可能地减少其重绘或回流的次数,只更改必要的元素。例如一个position设置为absolute/fixed的元素的更改只会影响其本身和其子元素,而static的元素变化则会影响其之后的所有页面元素。

另外一项优化的技术则是在JavaScript代码运行时,浏览器会缓存所有的变化,然后只通过一次pass绘制操作来应用这些更改。例如下面这段代码只会触发一次重绘和回流:

1
2
3
4
5
var $body = $('body');
$body.css('padding', '1px'); // 触发重绘与回流
$body.css('color', 'red'); // 触发重绘
$body.css('margin', '2px'); // 触发重绘与回流
// 最终只有一次重绘和回流被触发

然而,根据我们之前提到过的,获取某个元素的属性将会触发强制回流。比如我们在刚才的代码中加上一句读取元素属性的操作:

1
2
3
4
5
var $body = $('body');
$body.css('padding', '1px');
$body.css('padding'); // 此处触发强制回流
$body.css('color', 'red');
$body.css('margin', '2px');

结果就会有两次回流发生。因此,我们应该尽量合并读取元素属性的操作来优化性能。

当然也有我们不得不触发强制回流的情况。比如说对同一个元素的margin-left属性进行两次操作——开始的时候赋值100px的距离,之后为了实现动画效果,再加上transition属性将距离改变到50px.

我们先定义一个CSS类:

1
2
3
4
5
6
.has-transition {
-webkit-transition: margin-left 1s ease-out;
-moz-transition: margin-left 1s ease-out;
-o-transition: margin-left 1s ease-out;
transition: margin-left 1s ease-out;
}

之后再对页面元素进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 我们的元素开始默认含有 "has-transition" 的class属性
var $targetElem = $('#targetElemId');

// 移除默认的 "has-transition"
$targetElem.removeClass('has-transition');

// 此处的属性改变没有动画效果
$targetElem.css('margin-left', 100);

// 再加上原来的属性名
$targetElem.addClass('has-transition');

// 这次改变有动画效果
$targetElem.css('margin-left', 50);

但事实上这段代码并不会像注释描述的那样运作,每条语句的操作将被缓存,只有结果会在页面上显示,所以我们就需要手动进行一次强制回流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 移除默认的 "has-transition"
$(this).removeClass('has-transition');

// 此处的属性改变没有动画效果
$(this).css('margin-left', 100);

// 触发强制回流,上述两条语句的效果会马上在页面中显示
$(this)[0].offsetHeight; // 只是举个例子,别的触发方法也可以

// 再加上原来的属性名
$(this).addClass('has-transition');

// 这次改变有动画效果
$(this).css('margin-left', 50);

5.优化渲染效率的几条最佳实践

1.合法地书写HTML和CSS,不要忘了文档编码类型。样式文件应当在 标签中,脚本文件在 结束前。

2.简化并优化你的CSS选择器(有些人可能CSS预处理器用习惯了从来不关注这一点)。将嵌套层减少到最小。CSS选择器根据其优先级具有不同的运行效率(从快到慢):

  • ID选择器: #id
  • 类选择器: .class
  • 标签选择器: div
  • 相邻选择器: a + i
  • 子元素选择器: ul > li
  • 通用选择器: *
  • 属性选择器: input[type=”text”]
  • 伪类选择器: a:hover

浏览器中CSS选择器是从右到左进行匹配的(为什么浏览器要从右到左匹配样式选择器),这也是为什么越短的选择器运行越快的原因(别提通用选择器,它会遍历所有元素):

1
2
3
4
div * {...} // ×
.list li {...} // ×
.list-item {...} // √
#list .list-item {...} // √

3.在你的脚本代码中,尽量减少DOM操作。缓存所有的内容,包括属性和对象(如果他们需要被复用的话)。尽量将元素缓存到本地之后再进行操作,最后再添加到DOM当中。

4.如果你使用jQuery进行DOM操作的话,最好遵循jQuery最佳实践。

5.修改元素样式时,更改其class属性是性能最高的方法。你的选择器越有针对性越好(这同样也有助于分离页面样式和逻辑)。

6.尽量只对 position 为 absolute/fixed 的元素设置动画。

7.在页面滚动时禁用 :hover 样式效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.disable-hover {
pointer-events: none;
}
var body = document.body,
timer;

window.addEventListener('scroll', function() {
clearTimeout(timer);
if(!body.classList.contains('disable-hover')) {
body.classList.add('disable-hover')
}

timer = setTimeout(function(){
body.classList.remove('disable-hover')
},500);
}, false);