浏览器渲染机制-从输入url到渲染出页面的整个过程
前言
浏览器是我们日常开发的重要的工具,那么你了解浏览器吗?即使在前端面试中,我们也经常会遇到:在浏览器地址中从输入url地址到出现页面,这个过程发生了什么?介绍一下重绘和回流?这一类关于浏览器的问题。我们可能会知道大概的轮廓但对于具体的细节却是不那么清楚,那么今天我们就从浏览器组成开始来了解一下浏览器的渲染机制
浏览器组成
浏览器主要由7个部分组成:
- 用户界面(User Interface):定义了一些常用的浏览器组件,比如地址栏,返回、书签等等
- 数据持久化(Data Persistence):指浏览器的cookie、local storage等组件
- 浏览器引擎(Browser engine):平台应用的相关接口,在用户界面和呈现引擎之间传送指令。
- 渲染引擎(Rendering engine):处理HTML、CSS的解析与渲染
- JavaScript解释器(JavaScript Interpreter):解析和执行JavaScript代码
- 用户界面后端(UI Backend):指浏览器的的图形库等
- 网络(Networking):用于网络调用,比如HTTP请求
浏览器内核
浏览器内核分为两部分:渲染引擎(layout engineer或Rendering Engine)和JS引擎
·渲染引擎:负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入CSS等),以及计算网页的显示方式,然后会输出至显示器或打印机
·JS引擎:负责解析和执行javascript来实现网页的动态效果 浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核,最开始渲染引擎和JS引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎
常见的浏览器内核:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)
整个过程
加载过程
-
DNS解析:
将域名解析成ip地址,因为域名只是一个中间的服务号,真正在网络中还是通过ip地址做网络访问的 -
浏览器根据ip地址向服务器发起http请求
-
服务器处理http请求,并返回给浏览器
下载资源
从服务器中获取所需的资源,
要加载的资源形式:
渲染过程
根据HTML代码生成DOM Tree
这个解析过程大概可以分为几个步骤
第一步:浏览器从磁盘或网络读取HTML的原始字节,也就是传输的0和1这样的字节数据,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。(在网络中传输的内容其实都是 0 和 1 这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。)
第二步:将字符串转换成Token,例如:<html>、<body>等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息。
第三步:在每个Token被生成后,会立刻消耗这个Token创建出节点对象,因此在构建DOM的过程中,不是等待所有的Token都生成后才去构建DOM,而是一边生成Token一边消耗来生成节点对象。(DOM树的构建是从接受到文档开始的,一边会进行将字节转化为字符,字符转化为标记(Token),标记构建dom树,这个过程被分为标记化和树构建,而这是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,呈现引擎会将部分内容解析并显示出来。
)(注意:带有结束标签标识的Token不会创建节点对象 )
第四步:通过“开始标签”与“结束标签”来识别并关联节点之间的关系。当所有Token都生成并消耗完毕后,我们就得到了一颗完整的DOM树。
节点之间的关联关系是如何维护的呢?事实上,这就是Token要标识“起始标签”和“结束标签”等标识的作用。
以下图为例:“Hello”Token位于“title”开始标签与“title”结束标签之间,表明“Hello”Token是“title”Token的子节点。同理“title”Token是“head”Token的子节点。
接下来我们举个例子,假设有段HTML文本:
上面这段HTML会解析成这样:
根据CSS代码生成CSSOM
DOM会捕获页面的内容,但浏览器还需要知道页面如何展示,所以需要构建CSSOM。解析css构建CSSOM 的过程和构建DOM的过程非常的相似。当浏览器接收到一段CSS,浏览器首先要做的是识别出Token,然后构建节点并生成CSSOM
在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。为了CSSOM的完整性,也只有等构建完毕才能进入到下一个阶段,哪怕DOM已经构建完,它也得等CSSOM,然后才能进入下一个阶段。
CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去。所以CSS的加载速度与构建CSSOM的速度将直接影响首屏渲染速度,因此在默认情况下CSS被视为阻塞渲染的资源
备注:生成DOM Tree和CSSOM是两个线程的,没有固定的先后顺序
将DOM Tree和CSSOM整合形成Render Tree(包含每个节点的视觉信息)
当我们生成DOM树和CSSOM树后,我们需要将这两颗树合并成渲染树。(在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none的,那么就不会在渲染树中显示。)
在构建渲染树的过程中浏览器需要做如下工作:
·从 DOM 树的根节点开始遍历每个可见节点。
·有些节点不可见(例如脚本Token(如
渲染阻塞
在渲染的过程中,遇到一个script标记时,就会停止渲染,去请求脚本文件并执行脚本文件,因为浏览器渲染和 JS 执行共用一个线程,而且这里必须是单线程操作。为何要共用一个线程?因为JS可能会改变Render Tree的结构,多线程会产生渲染 DOM 冲突。
JavaScript的加载、解析与执行会严重阻塞DOM的构建。也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。
也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性(下文会介绍这两者的区别)。
JS文件不只是阻塞DOM的构建,它会导致CSSOM也阻塞DOM的构建。原本DOM和CSSOM的构建是互不影响,井水不犯河水,但JavaScript不只是可以改DOM,它还可以更改样式,也就是它可以更改CSSOM。因为不完整的CSSOM是无法使用的,如果JavaScript想访问CSSOM并更改它,那么在执行JavaScript时,必须要能拿到完整的CSSOM。所以就导致了一个现象,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和DOM构建,直至其完成CSSOM的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后在继续构建DOM。
因此script的位置很重要,在实际使用过程中遵循以下两个原则:
CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
JS置后:我们通常把JS代码放到页面底部,且JavaScript 应尽量少影响 DOM 的构建
布局与绘制
当浏览器生成渲染树以后,就会根据渲染树来进行布局(layout)(也可以叫做回流(flow))。
布局阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小,通常这一行为也被称为“自动重排”。
布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。
布局完成后,浏览器会立即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素,即绘制(paint)
“生成布局layout”(flow)和"绘制"(paint)这两步,合称为"渲染"(render),即:浏览器根据Render Tree渲染页面。
渲染的流程基本上是这样(如下图黄色的四个步骤):
- 计算CSS样式
- 构建Render Tree
- Layout – 定位坐标和大小
- 正式开画
注意:上图流程中有很多连接线,这表示了Javascript动态修改了DOM属性或是CSS属性会导致重新Layout,但有些改变不会重新Layout,就是上图中那些指到天上的箭头,比如修改后的CSS rule没有被匹配到元素。
重绘与回流
这里重要要说两个概念,一个是Reflow,另一个是Repaint:
重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。
回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)
我们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断重新渲染。重新渲染会重复回流+重绘或者只有重绘。
回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
常见引起回流属性和方法:
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流
- 添加或者删除可见的DOM元素;
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容变化,比如用户在input框中输入文字、文本变化或图片被另一个不同尺寸的图片所替代
- 浏览器窗口尺寸改变——resize事件发生时
- 计算 offsetWidth 和 offsetHeight 属性
- 设置 style 属性的值
- 页面一开始渲染的时候(这肯定避免不了)
常见引起重绘属性和方法:
如何减少回流、重绘:
- 使用 transform 替代 top
- 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
- 不要把节点的属性值放在一个循环里当成循环里的变量。
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
- 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
- CSS 选择符从右往左匹配查找,避免节点层级过多
- 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。
- 将动画效果应用到position属性为absolute或fixed的元素上
javascript - 避免频繁操作样式,可汇总后统一 一次修改
- 尽量使用class进行样式修改
- 减少dom的增删次数,可使用 字符串 或者 documentFragment 一次性插入
- 极限优化时,修改样式可将其display: none后修改
- 避免多次触发上面提到的那些会触发回流的方法,可以的话尽量用 变量存住
性能优化策略
基于上面介绍的浏览器渲染原理,DOM 和 CSSOM 结构构建顺序,初始化可以对页面渲染做些优化,提升页面性能。
JS优化:
CSS优化:标签的 rel属性 中的属性值设置为 preload 能够让你在你的HTML页面中可以指明哪些资源是在页面加载完成后即刻需要的,最优的配置加载顺序,提高渲染性能。
async和defer的作用是什么?有什么区别
其中蓝色线代表JavaScript加载;红色线代表JavaScript执行;绿色线代表 HTML 解析。
情况1:<script src=“script.js”></script>
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。
情况2<script async src=“script.js”></script>(异步下载)
async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。
情况3 <script defer src=“script.js”></script>(延迟执行)
defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。
defer 与相比普通 script,有两点区别:
- 载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后;
- 在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载
js优化可以在script标签加上 defer属性 和 async属性用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行
首屏优化加载
- 减少首屏CGI的计算量:比如在微信8.8无现金日H5开发中,前端希望拿到用户的个人信息、消费记录、排名三类数据,如果只通过一个CGI来处理,那么后台响应时间肯定会变长;由于在H5的首屏中,只包含了用户信息,消费记录、排名都在第2屏和第3屏,此时其实可以利用异步的方式来拿消费记录、排名的数据。
- 页面瘦身:压缩HTML、CSS、JavaScript。
- 减少请求:CSS、JavaScript文件数尽量少,甚至当CSS、JS的代码不多时,可以考虑直接将代码内嵌到页面中。
- 多用缓存:缓存能大幅度降低页面非首次加载的时间。
- 少用table布局,浏览器在渲染table时会消耗较多资源,而且只有table里有一点变化,整个table都会重新渲染。
- 做预加载:部分H5页面首屏可能要下载较多的静态资源,比如图片,这时为了避免加载时出现“难看”的页面,用预加载(loading的方式)做一个过渡
- 使用 CDN 等
- 使用内联 JS、CSS ,减少 JS 、 CSS 文件的下载
- webpack 等工具对 JS、CSS 文件压缩,减少文件大小
总结
我们已经将浏览器的渲染机制了解了一遍,不仅了解到一些性能优化方案,也可以得出结论: 浏览器渲染的关键路径共分五个步骤:
构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制
补充
1、DOM(Document Object Model)和CSSOM(CSS Object Model)是一种树形的数据结构。
2、渲染树不止是简单的DOM树+CSSOM树,DOM树中的head等节点和不需要渲染在页面上的节点(display:none)都不会出现在Render树中。
3、加载资源,如img、CSS文件等都是以异步请求的方式实现的,不会影响到元素的渲染。而请求JavaScript文件是先将渲染进程挂起,等加载完毕再继续渲染(又称渲染阻塞)。这是因为JS文件中可能出现很多DOM操作,这样能够提高性能。另外,加载JS文件最好写在HTML文件底部,否则容易导致对DOM的操作时,DOM还未生成。
4、以上几个步骤因为DOM、CSSOM、Render Tree都可能在第一次Painting后又被更新多次,比如JS修改了DOM或者CSS属性。Layout和Painting也会被重复执行,除了DOM、CSSOM更新的原因外,图片下载完成后也需要调用Layout 和 `Painting来更新网页
https://blog.csdn.net/weixin_44019523/article/details/116851254
https://juejin.cn/post/6844903846834094094
https://www.csdn.net/tags/NtzaUg2sMDEyOTItYmxvZwO0O0OO0O0O.html