一般来说,从输入 url 到返回数据,大致经历以下几个过程:
输入域名 -> DNS 解析(域名解析) -> 找到 IP 地址 -> 返回 HTML 文件。
渲染的概念
在开始正文之前,我们先了解一下渲染的概念:在网页开发中,渲染指的是将代码 ( HTML CSS JavaScript ) 转化为用户可交互的一些页面,该过程一般是由浏览器引擎来完成的。简单来说,渲染就是浏览器将代码变成人眼看到图像的全过程。
浏览器是如何呈现页面的
- 当浏览器接收到这样一个
HTML文件之后,他收到的不是里面的内容,而是文件的字节数据 - 而接收到这些数据字节之后,根据文件的指定编码(例如 UTF-8 ) 以及一些转化让他们最终形成一系列标记(
token),例如<html><body>,标记里面含有这个标签的基本信息,例如开始和结束。 - 浏览器引擎将这些标记解析成为节点,并会根据标签的类型创建不同类型的节点对象,并为他们分配属性和方法。创建好之后,节点会被链接到称为
DOM(文档对象模型)的树状结构当中,每一个节点之间都建立了联系,包含了节点的基本信息
-
此外,
HTML代码中往往回引入一些额外的资源,例如CSS、图片、javaScript脚本等,其中CSS和图片的下载需要通过网络进行或者直接从缓存中获取,他们的下载不会阻塞DOM树的生成- 下载完毕
CSS之后,浏览器引擎会以类似于生成DOM的方式,形成CSSOM(CSS 对象模型),它包含着每一个节点的样式信息, - 而在一般情况下,如果遇到
scrpt标签则会停止HTML解析流程,转而进行下载和加载解析javaScript代码,所以他会阻塞DOM树的生成。(详情可以见下文script标签)
- 下载完毕
- 当
DOMCSSOM构建完毕后,浏览器引擎会结合起来创建一个称为渲染树 (render tree) 的树状结构。渲染树上包含了每个元素的相关信息,例如样式和内容。
(也就是说:下载 CSS形成CSSOM不会阻塞DOM tree的构建过程,但会阻塞Render Tree的构建过程)
- 最后,浏览器引擎通过解析渲染树,在渲染树上运行布局(用来确定渲染树上所有节点的宽度,高度,尺寸,位置信息),然后通过添加或绘制元素从而呈现一个完整的页面。
回流(Reflow)和重绘 (Repaint)
刚才提到,在 render树生成之后,还需要运行布局(layout)用来确定树上节点的宽高位置,运行绘制(painting)对元素的颜色样式进行绘制然后对内容进行渲染,而对页面元素的布局位置和样式进行重新更改的操作,则被称之为回流和重绘
- 回流是指当浏览器在渲染页面时,发现了某个元素的位置和大小发生了变化,需要重新计算整个渲染树的布局信息,然后将元素重新渲染到页面上的过程
- 重绘(repaint)是指当浏览器在渲染页面时,发现某个元素的样式发生了变化,需要重新绘制元素的内容到页面上的过程
做什么操作会引起回流?
DOM结构发生改变(添加新的节点或者移除节点)
改变了布局 (修改了 width、height、padding、font-size等值)
修改了窗口的尺寸等
怎么触发重绘
修改了背景颜色,文字颜色等等样式
如何避免回流 ?
为什么要避免回流?从刚才的解释就可以看出来,回流是一项非常昂贵的操作,因为它涉及到整个渲染树的重新计算和布局,会消耗大量的计算资源和时间,尽可能减少回流的次数,是提高网页性能的重要手段之一。
- 避免设置多层内联样式。
- 如果需要设置动画效果,最好将元素脱离正常的文档流,比如设置
position的absolute或fixed,使得开销相对较少,不会影响其他元素 - 避免频繁操作样式,最好一次性修改样式,比如可以添加 class修改
- 避免频繁操作
DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
display, opacity,visibility
刚才我们讲到,有一些属性会引起回流和重绘,我在查阅资料的过程中,看到了 display: none opactiy: 0 以及 visibility:hidden ,他们在理论上都能使得元素在视图上看不见,但他们在引发回流和重绘上有着些许的不同。
display:none
通过大家的使用,我相信都知道,display 设置为 none后,元素会直接在视图上消失,并且没有占位,所以我们不难得出,当更改元素为 display:none 之后,他会引发回流和重绘。那这是为什么 ?
原因很简单:当元素的 display 变为 none之后,他会将元素从 render 树上直接移除,而由于render 树上保存着节点的基本信息,它发生了改变,那么自然会对页面进行重新布局和绘制。
visibility: hidden
visibilityCSS 属性显示或隐藏元素而不更改文档的布局。
对于 visibility:hidden 来说,它与 display:none 最大的不同在于它不会舍弃掉元素原有的占位位置,它只是让元素变得不可见。所以说,它的改变会触发浏览器的重绘
opacity: 0
先看一下 MDN 对于这个属性的解释:
opacity 属性指定了一个元素的不透明度。换言之,opacity 属性指定了一个元素后面的背景的被覆盖程度。
他与上述属性最大的不同在于:更改 opacity 属性 是不会引起回流重绘的,原因在于,这个属性会创建一个合成层(详见下文),直接通过 GPU 进行加速渲染的,因此不会进行回流和重绘。
合成层 (compositing layers)
在我最开始查阅资料并写到这里的时候,我理所当然得出了一个简单的结论,浏览器会将布局后的所有元素会一次性绘制到一个图层里,但是实际上这个说法并不准确,浏览器里经常会有动画,video,canvas 等等东西,页面也会一直变动,如果每次变动都对整个图层进行重绘,那这样子对于性能的开销岂不是很大?由此我通过查找其他资料,了解了合成层这一概念。
合成层是什么 ?
在 DOM 树中每个节点都会对应一个 RenderObject(渲染对象),当他们的 RenderObject处于相同的坐标空间时,就会形成一个 RenderLayer,也就是渲染层,渲染层将保证页面元素以正确的顺序堆叠,这时候就会出现层合成(composite),从而保证各个元素能在页面上正常显示
在默认情况下,标准流的内容会被绘制在同一图层当中,满足某些特殊条件的渲染层,会被浏览器自动提升为合成层(compositing layers), 这些合成层都会被单独渲染,借助于GPU的图形处理能力进行单独处理和渲染,从而加速页面的绘制和更新 。
从图中不难看出,创建一个新合成层的元素和其他元素已经不在同一图层了。
哪些属性可以形成新的合成层
- 3D
transforms:translate3d,translateZ等; video,canvas,iframe等元素;opacity- 通过
СSS动画实现的opacity动画转换; position: fixed;will-change(该属性会将元素提取到一个新层,让浏览器提前做好准备)filter
总的来说,形成合成层可以让该元素的变动不影响其他元素,并且在一定程度上提高性能,但并不代表它是完美的,性能的提高带来的是内存消耗的变大。
Script 标签的解析
正如上面例子所示,当浏览器在解析 HTML 的时候,一旦遇到 script 元素,那么浏览器不会继续构建 DOM 树,他会停止继续构造, 直至 JavaScript 下载完成。
为什么要这么做?后果是什么?
原因其实很简单,script 标签当中为 JavaScript 代码,JavaScript 的作用之一就是操作DOM,并且可以修改DOM,
但如果我们等到DOM 树构建完成并且渲染再执行 JavaScript ,会造成严重的回流和重绘,影响页面的性能;
所以说,遇到 script 标签时,会优先下载和执行 JavaScript 代码,再继续构建 DOM 树,即 script 标签下载,执行完成之前,用户什么也看不到。为了解决这种问题,script 提供了两种属性 defer 与 async
Defer 属性
- 携带了
defer属性的script标签加载是异步的,加载完成之后也不会马上执行,而是等待HTML解析完毕之后再执行,所以执行也不会阻塞HTML的渲染。 - 如果脚本提前下载好了,那么它会等待
DOM构建完成,在DOMContentLoaded事件之前先执行defer属性脚本 中的代码,有defer属性的脚本会阻止DOMContentLoaded事件,直到脚本被加载并且解析完成。 - 多个带 defer 的脚本是可以保持正确的顺序执行的。
当初始的 HTML 文档被完全加载和解析完成之后,
DOMContentLoaded事件被触发,而无需等待样式表、图像和子框架的完全加载。
注意:defer 仅适用于外部脚本,对于 script 默认内容会被忽略。
Async 属性
Async 与 defer 类似,都可以告诉浏览器不必等待脚本下载,继续解析 HTML 。
不同点在于
async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本;async不一定在DOMContentLoaded之前或者之后执行
简单来说,如果需要在 HTML 解析后操作 DOM ,并且对于顺序有一定要求,可以添加 defer,async 则通常用于独立的脚本。
其他属性
integrity: 包含用户代理可用于验证已提取资源是否已无意外操作的内联元数据。简单来讲,它可以让浏览器在下载 js 文件的时候,进行一个验证。type: 用于定义脚本语言的类型,MIME 包括 text/javascript 、text/ecmascript 、application/javascript 等等crossorigin:浏览器启用 CROS 访问检查,属性值包括 anonymous 、 use-credentials,若未指定属性值或非法属性值,浏览器默认采用 anonymous- src:这个属性定义引用外部脚本的 URI,这可以用来代替直接在文档中嵌入脚本
总结
总的来说,浏览器的渲染过程可以用这张图来表示:
写在最后
本篇文章虽然探讨了浏览器的渲染原理,但还是有不少地方讲的很浅,比如合成层部分,还有更多深层次的东西没有涉及到。此外,有些地方也可能会有讲错或者理解错的地方,希望大家能指出我的错误,一起进步 !
参考资料
www.seobility.net/en/wiki/Ren…