浏览器解析渲染以及页面优化

浏览器对页面的加载、解析以及渲染是一个非常复杂的话题,这里我只对这个过程做一个非常概括的总览,以初步了解这些过程,对于前端编程如何优化页面给予一些原理性的支撑。

为什么要了解这些过程
  • 了解浏览器的资源加载,可以使我们在引入外部样式、脚本资源时进行更合理的时机选择

  • 了解浏览器文档解析,可以使我们在构建DOM结构,组织CSS选择器等,选择更为合理的写法

  • 了解浏览器结果渲染,让我们在设置元素属性,脚本操作尽量优化以减少和避免不必要的重绘和回流

以上三个过程并非完全独立,而是会有交叉,存在一边加载一边解析一边渲染的工作现象。

加载

从用户输入网址,这其后到获得网页文档之前也包含了如下众多步骤:

  • DNS解析,会依次从缓存,本地Hosts,本地DNS服务器直至根DNS服务器,任何一级命中结果则直接返回,此步骤获得目标服务器IP

  • 端口解析,从URL中获得目标服务器端口号

  • 浏览器建立一条与网页服务器的TCP连接(三次握手)

  • 浏览器向服务器发送一条HTTP请求报文

  • 服务器执行一些逻辑如数据库查询并最终生成并返回HTTP响应报文

初步经过以上步骤,浏览器获得一份HTML文档,然后浏览器会对此文档自上而下进行加载,加载过程中就同时进行解析和渲染。

加载过程中,遇到外部的CSS资源,浏览器会通过额外的资源线程发出请求,获取CSS文件,同样如果遇到图片资源,浏览器会发出另一个请求,获取图片资源,这些请求都是异步的,并不影响HTML文档继续加载。但是如果遇到了JS资源,HTML文档会暂停解析,要等待JS资源加载并解析执行完毕,才会重新恢复HTML文档解析。

浏览器这样设计的考量是JS很有可能修改DOM结构,这意味着JS脚本后的一些资源加载、文档解析可能是不必要的工作。这里就衍生出常见的一条优化法则: 将脚本文件加载置于文档末尾'body'闭合标签之前,或者在脚本引入代码上添加defer或async属性,以表明它们是可以异步加载

此外,在HTTP1.X中,同一个域名下并行资源请求的数量是很有限的,不同浏览器的限制不一定一致,对于这一点又衍生出多条优化方案:

将资源请求合并,如利用雪碧图将多个图片合为一个图片使用

启用静态资源专用域名,扩大浏览器同时进行资源请求的数量

虽然CSS资源的加载并不影响JS资源的加载,但是却会影响JS的执行:

这是因为JS内部可能会执行一些依赖于CSS样式的操作,如获得元素尺寸,所以JS执行前,浏览器务必保证CSS资源下载和解析完成。所以如果有需要提前执行的JS脚本,可以将JS引入置于CSS引入之前

解析

浏览器解析HTML文档主要包含如下几个部分:

  • 将HTML文档解析为DOM Tree,是由DOM元素以及属性节点组成,树的根是Document对象

  • 将CSS解析为样式表对象,是CSS选择器以及对象CSS规则的集合

  • JS脚本解析,可能会对DOM Tree以及元素样式进行更改,并最终反映到DOM Tree和CSS样式表对象的更新里面

渲染

此过程即为浏览器构建渲染树(Render Tree)的过程,渲染树是DOM树的可视化表示,是DOM数与样式表对象的结合,所以DOM树中一些不可见元素不会插入到渲染树中,如<head>display:none的元素等。一些脱离了文档流的元素(使用了绝对或浮动的元素等)在两棵树上的位置不完全一致,渲染树会标识出真实的位置,而只在原有位置放置占位标识。

渲染过程中,浏览器要为每个元素找出在样式表对象中定义的不定数量的样式规则。这里就需要指出CSS选择器匹配的问题,多重选择器是从右向左进行匹配,选择器太深不利于样式查找。

渲染树创建完毕后,则继续进行布局(Layout)。这个过程根据渲染对象的信息,计算出每个渲染对象的位置、尺寸,将其放置在浏览器窗口的正确位置。如果在布局完成之后又进行了DOM修改,这时候可能需要重新布局。每个渲染对象都有其自身的布局方法,同样渲染树的布局可以是全局也可以是局部,比如窗口尺寸改变或修改根元素尺寸或字体大小等,将会带来全局的重新布局,如果只改变了内部某一块渲染对象,则可能带来一个局部重新布局。

布局完成之后,浏览器会遍历上步获得的呈现树,不断调用呈现器的绘制方法,将对应呈现器的内容显示在屏幕上。

下图指示了WebKit浏览器解析渲染的过程

Webkit网页解析渲染流程

此外,还需要说明两个概念,重绘(Repaint)回流(Reflow)

Repaint: 不影响布局的属性变更,如背景色
Reflow: 元素的几何尺寸变了,需要重新验证并计算Render Tree。是Render Tree的一部分或全部发生了变化。这就是Reflow,或是Layout

Reflow的成本比Repaint的成本高得多的多。DOM Tree里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow。在一些高性能的电脑上也许还没什么,但是如果reflow发生在手机上,那么这个过程是非常痛苦和耗电的。 所以,下面这些动作有很大可能会是成本比较高的。

  • 当你增加、删除、修改DOM结点时,会导致Reflow或Repaint

  • 当你移动DOM的位置,或是搞个动画的时候。

  • 当你修改CSS样式的时候。

  • 当你Resize窗口的时候(移动端没有这个问题),或是滚动的时候。

  • 当你修改网页的默认字体时。

  • display:none会触发reflow,而visibility:hidden只会触发repaint,因为没有发现位置变化。

基本上来说,reflow有如下的几个原因:

  • Initial: 网页初始化的时候。

  • Incremental: 一些Javascript在操作DOM Tree时。

  • Resize: 其些元件的尺寸变了。

  • Style Change: 如果CSS的属性发生变化了。

  • Dirty: 几个Incremental的reflow发生在同一个frame的子树上。

浏览器对于频繁的重绘和回流做了一些优化,比如,浏览器会把回流操作积攒一批,然后做一次回流,这又叫异步回流或增量异步回流。但是有些情况浏览器是不会这么做的,比如:resize窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行回流。

页面渲染优化

虽然浏览器已经极尽所能会对页面渲染做一些针对性的优化,但是在了解浏览器的文档加载渲染和解析过程之后,我们需要对自己编写的代码做一些相对应的优化和更佳的实践:

  • 简化DOM层次结构
  • 简化CSS选择器结构
  • 脚本后置
  • 少量首屏样式可内联
  • 脚本中减少不必要的DOM操作,尽量缓存DOM查找以及DOM样式信息
  • 尽量通过修改类名或动画来修改元素样式
  • 动画尽量使用在绝对或固定定位元素上
  • 屏幕外不可见部分或滚动时的动画尽量停止