想要成为一名合格的前端工程师,掌握相关浏览器的工做原理是必备的,这样子才会有一个完整知识体系,要是「能参透浏览器的工做原理,你就能解决80%的前端难题」。css
「其余文章:」html
❝❞
[实用👍]推荐一些很是棒的前端网站 [干货👍]从详细操做js数组到浅析v8中array.js [诚意满满👍]Chrome DevTools调试小技巧,效率➡️🚀🚀🚀 [建议👍]再来100道JS输出题酸爽继续(共1.8W字+巩固JS基础) [1.2W字👍]写给女朋友的秘籍-浏览器工做原理(上)篇 面试如何写出一个满意的深拷贝(适合初级前端)
「这篇文章准备梳理一下渲染流程,也就是浏览器是怎么把HTML,CSS,JS,图片等资源最后显示漂亮的页面。」前端
❝仍是那句话,「了解浏览器是如何工做的,能让你站在更高维度去理解前端」html5
❞
❝但愿经过这篇文章,可以让你从新认识浏览器,并把JavaScript,网络,页面渲染,浏览器安全等知识串联起来,从而让你对整个前端体系有全新的认识。web
❞
「这篇主要是梳理了渲染流程中几个重要的步骤,以及从中有哪些优化的点,怎么样避免和减小重绘、重排,对优化性能上有必定的帮助。」面试
「读完这一期内容,你将收获」算法
小声说:欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程👊chrome
讲浏览器中渲染流程,先把上一篇所梳理的内容大概回顾一下segmentfault
上一篇文章:[1.2W字👍]写给女朋友的秘籍-浏览器工做原理(上)篇api
Http请求的总体流程,大体分为八个阶段
八个阶段归纳为: 构建请求 查找缓存 准备 IP 和端口 等待 TCP 队列 创建 TCP 链接 发起 HTTP 请求 服务器处理请求 服务器返回请求和断开链接
每一个阶段大体的工做原理梳理了一下
浏览器缓存顺带提起了一下(面试常考) 性能优化一部分
渲染进程接受到CommitNavigation消息以后,便开始与网络进程创建数据管道提及,此时渲染进程开始干活了
那么才有了咱们接下来要梳理的内容,渲染进程如何工做的呢
首先要了解的概念:
渲染引擎:它是浏览器最核心的部分是 “Rendering Engine”,不过咱们通常习惯将之称为 “浏览器内核”
渲染引擎主要包括的线程:
各个线程主要职责
以上来自阿宝哥总结:你不知道的 Web Workers (上)[7.8K 字 | 多图预警]
有了上述的概念,对接下咱们讲渲染流水线会有所帮助
好久以前就把浏览器工做原理读完了,看了不少博客,文章,当时简简单单的梳理一些内容,以下👇
简略版渲染机制通常分为如下几个步骤
接下来大概就是这么说:
在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。而且构建 CSSOM 树是一个十分消耗性能的过程,因此应该尽可能保证层级扁平,减小过分层叠,越是具体的 CSS 选择器,执行速度越慢。
当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方从新开始。也就是说,若是你想首屏渲染的越快,就越不该该在首屏就加载 JS 文件。而且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,因此也能够认为这种状况下,CSS 也会暂停构建 DOM。
说完这些,记下来就讲几个面试经常会提起的,会问你的知识点👇
Load 事件触发表明页面中的 DOM,CSS,JS,图片已经所有加载完毕。
DOMContentLoaded 事件触发表明初始的 HTML 被彻底加载和解析,不须要等待 CSS,JS,图片加载。
通常来讲,能够把普通文档流当作一个图层。特定的属性能够生成一个新的图层。不一样的图层渲染互不影响,因此对于某些频繁须要渲染的建议单独生成一个新图层,提升性能。但也不能生成过多的图层,会引发副作用。
经过如下几个经常使用属性能够生成新图层
translate3d
、translateZ
will-change
video
、iframe
标签opacity
动画转换position: fixed
重绘和回流是渲染步骤中的一小节,可是这两个步骤对于性能影响很大。
color
就叫称为重绘回流一定会发生重绘,重绘不必定会引起回流。回流所需的成本比重绘高的多,改变深层次的节点极可能致使父节点的一系列回流。
因此如下几个动做可能会致使性能问题:
不少人不知道的是,重绘和回流其实和 Event loop 有关。
resize
或者 scroll
,有的话会去触发事件,因此 resize
和 scroll
事件也是至少 16ms 才会触发一次,而且自带节流功能。requestAnimationFrame
回调IntersectionObserver
回调,该方法用于判断元素是否可见,能够用于懒加载上,可是兼容性很差requestIdleCallback
回调。以上内容来自于 HTML 文档
使用 translate
替代 top
<div class="test"></div>
<style>
.test {
position: absolute;
top: 10px;
width: 100px;
height: 100px;
background: red;
}
</style>
<script>
setTimeout(() => {
// 引发回流
document.querySelector('.test').style.top = '100px'
}, 1000)
</script>
复制代码
使用 visibility
替换 display: none
,由于前者只会引发重绘,后者会引起回流(改变了布局)
把 DOM 离线后修改,好比:先把 DOM 给 display:none
(有一次 Reflow),而后你修改100次,而后再把它显示出来
不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会致使回流,由于须要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}
复制代码
不要使用 table 布局,可能很小的一个小改动会形成整个 table 的从新布局
动画实现的速度的选择,动画速度越快,回流次数越多,也能够选择使用 requestAnimationFrame
CSS 选择符从右往左匹配查找,避免 DOM 深度过深
将频繁运行的动画变为图层,图层可以阻止该节点回流影响别的元素。好比对于 video
标签,浏览器会自动将该节点变为图层。
上面的渲染流程是我一年前就在我笔记中存在的内容,我还记得当时学习的方式是囫囵吞枣式的,上面☝简略版的渲染流程,我印象中是在GitHub上面某博看看的,当时直接Copy下来的,当时以为这个渲染原理这块有了别人梳理好的结论,本身多看看,会记住的,事实上,面试的时候,提起这部分的时候,深度明显不够,天然就被问倒了
下来梳理了一份详细的版本,坦白说,做为一个学者,天然是站在巨人的肩膀上,去总结梳理知识,我认为这是对我最有效的学习方式
让我带着你🚗重温通常渲染流程吧
较为专业的术语总结为如下阶段:
你能够想象一下,从0,1字节流到最后页面展示在你面前,这里面渲染机制确定很复杂,因此渲染模块把执行过程当中化为不少的子阶段,渲染引擎从网络进程拿到字节流数据后,通过这些子阶段的处理,最后输出像素,这个过程能够称为渲染流水线
,咱们从一张图上来看👇
那接下来就从每一个阶段来梳理一下大体过程。
这个过程主要工做就是讲HTML内容转换为浏览器DOM树结构
文档对象模型(DOM)
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>Critical Path</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
</body>
</html>
复制代码
咱们先看看数据是怎么样转换的👇
大概过程:
咱们把上述这样子的过程就叫作是构建DOM树过程
这个子阶段主要有三个步骤
咱们拿到的也就是0,1字节流数据,浏览器没法直接去识别的,因此渲染引擎收到CSS文本数据后,会执行一个操做,转换为浏览器能够理解的结构-styleSheets
若是你很想了解这个格式化的过程,能够好好去研究下,不一样的浏览器可能在CSS格式化过程当中会有所不一样,在这里就不展开篇幅了。
经过浏览器的控制台document.styleSheets
能够来查看这个最终结果。经过JavaScript能够完成查询和修改功能,或者说这个阶段为后面的样式操做提供基石。
什么是标准化样式表呢?先看一段CSS文本👇
body { font-size: 2em }
p {color:blue;}
span {display: none}
div {font-weight: bold}
div p {color:green;}
div {color:red; }
复制代码
有些时候,咱们写CSS 样式的时候,会写font-size:2em;color:red;font-weight:bold
,像这些数值并不容易被渲染引擎所理解,所以须要在计算样式以前将它们标准化,如em
->px
,red
->rgba(255,0,0,0)
,bold
->700
等等。
上面的代码标准后属性值是什么样子呢👇
经过格式化
和标准化
,接下来就是计算每一个节点具体样式信息了。
计算规则:继承
和层叠
继承
:每一个子节点会默认去继承父节点的样式,若是父节点中找不到,就会采用浏览器默认的样式,也叫UserAgent样式
。
层叠
:样式层叠,是CSS一个基本特征,它定义如何合并来自多个源的属性值的算法。某种意义上,它处于核心地位,具体的层叠规则属于深刻 CSS 语言的范畴,这里就补展开篇幅说了。
不过值得注意的是,在计算完样式以后,全部的样式值会被挂在到window.getComputedStyle
当中,也就是能够经过JS来获取计算后的样式,很是方便。
这个阶段,完成了DOM节点中每一个元素的具体样式,计算过程当中要遵循CSS的继承
和层叠
两条规则,最终输出的内容是每一个节点DOM的样式,被保存在ComputedStyle中。
想了解每一个 DOM 元素最终的计算样式,能够打开 Chrome 的“开发者工具”,选择第一个“element”标签,好比我下面就选择了div标签,而后再选择“Computed”子标签,以下图所示:
若是不是很理解的话,能够看这里👇
跟处理HTML同样,咱们须要更具CSS两个规则:继承
和层叠
转换成某种浏览器能理解和处理的东西,处理过程相似处理HTML,如上图☝
CSS 字节转换成字符,接着转换成令牌和节点,最后连接到一个称为“CSS 对象模型”(CSSOM) 的树结构内👇
不少人确定看这个很熟悉,确实,不少博客都是基于CSSOM说法来说的,我要说的是:
和DOM不同,源码中并无CSSOM这个词,因此不少文章说的CSSOM应该就是styleSheets,固然了这个styleSheets咱们能够打印出来的
不少文章说法是渲染树也是16年前的说法,如今代码重构了,咱们能够把LayoutTree当作是渲染树,不过它们之间仍是有些区别的。
上述过程已经完成DOM树(DOM树)构建,以及样式计算(DOM样式),接下来就是要经过浏览器的布局系统肯定元素位置,也就是生成一颗布局树(Layout Tree),以前说法叫 渲染树。
在DOM树上不可见的元素,head元素,meta元素等,以及使用display:none属性的元素,最后都不会出如今布局树上,因此浏览器布局系统须要额外去构建一棵只包含可见元素布局树。
咱们直接结合图来看看这个布局树构建过程:
为了构建布局树,浏览器布局系统大致上完成了下面这些工做:
接下来就是要计算布局树节点的坐标位置,布局的计算过程很是复杂,张开介绍的话,会显得文章过于臃肿,大多数状况下,咱们只须要知道它所作的工做是什么,想知道它是如何作的话,能够看看如下两篇文章👇
一图归纳上面三个阶段
首先须要知道的就是,浏览器在构建完布局树
后,还须要进行一系列操做,这样子可能考虑到一些复杂的场景,好比一些些复杂的 3D 变换、页面滚动,或者使用 z-indexing 作 z 轴排序等,还有好比是含有层叠上下文如何控制显示和隐藏等状况。
生成图层树
你最终看到的页面,就是由这些图层一块儿叠加构成的,它们按照必定的顺序叠加在一块儿,就造成了最终的页面。
浏览器的页面实际上被分红了不少图层,这些图层叠加后合成了最终的页面。
咱们来看看图层与布局树之间关系,以下图👇
一般状况下,并非布局树的每一个节点都包含一个图层,若是一个节点没有对应的层,那么这个节点就从属于父节点的图层。
那什么状况下,渲染引擎会为特定的节点建立新图层呢?
有两种状况须要分别讨论,一种是显式合成,一种是隐式合成。
显式合成
1、 拥有层叠上下文的节点。
层叠上下文也基本上是有一些特定的CSS属性建立的,通常有如下状况:
2、须要剪裁(clip)的地方。
好比一个标签很小,50*50像素,你在里面放了很是多的文字,那么超出的文字部分就须要被剪裁。固然若是出现了滚动条,那么滚动条也会被单独提高为一个图层,以下图
数字1箭头指向的地方,能够看看,可能效果不是很明显,你们能够本身打开这个Layers探索下。
元素有了层叠上下文的属性或者须要被剪裁,知足其中任意一点,就会被提高成为单独一层。
隐式合成
这是一种什么样的状况呢,通俗意义上来讲,就是z-index
比较低的节点会提高为一个单独的途图层,那么层叠等级比它高
的节点都会成为一个独立的图层。
缺点: 根据上面的文章来讲,在一个大型的项目中,一个z-index
比较低的节点被提高为单独图层后,层叠在它上面的元素通通都会提高为单独的图层,咱们知道,上千个图层,会增大内存的压力,有时候会让页面崩溃。这就是层爆炸
完成了图层的构建,接下来要作的工做就是图层的绘制了。图层的绘制跟咱们平常的绘制同样,每次都会把一个复杂的图层拆分为很小的绘制指令,而后再按照这些指令的顺序组成一个绘制列表,相似于下图👇
从图中能够看出,绘制列表中的指令其实很是简单,就是让其执行一个简单的绘制操做,好比绘制粉色矩形或者黑色的线等。而绘制一个元素一般须要好几条绘制指令,由于每一个元素的背景、前景、边框都须要单独的指令去绘制。
你们能够在 Chrome 开发者工具中在设置栏中展开 more tools
, 而后选择Layers
面板,就能看到下面的绘制列表:
在该图中,**箭头2指向的区域 **就是 document 的绘制列表,**箭头3指向的拖动区域 **中的进度条能够重现列表的绘制过程。
固然了,绘制图层的操做在渲染进程中有着专门的线程,这个线程叫作合成线程。
接下来咱们就要开始绘制操做了,实际上在渲染进程中绘制操做是由专门的线程来完成的,这个线程叫合成线程。
绘制列表准备好了以后,渲染进程的主线程会给合成线程
发送commit
消息,把绘制列表提交给合成线程。接下来就是合成线程一展宏图的时候啦。
你想呀,有时候,你的图层很大,或者说你的页面须要使用滚动条,而后页面的内容太多,多的没法想象,这个时候须要滚动很久才能滚动到底部,可是经过视口,用户只能看到页面的很小一部分,因此在这种状况下,要绘制出全部图层内容的话,就会产生太大的开销,并且也没有必要。
首屏渲染加速能够这么理解:
由于后面图块(非视口内的图块)数据要进入 GPU 内存,考虑到浏览器内存上传到 GPU 内存的操做比较慢,即便是绘制一部分图块,也可能会耗费大量时间。针对这个问题,Chrome 采用了一个策略: 在首次合成图块时只采用一个低分辨率的图片,这样首屏展现的时候只是展现出低分辨率的图片,这个时候继续进行合成操做,当正常的图块内容绘制完毕后,会将当前低分辨率的图块内容替换。这也是 Chrome 底层优化首屏加载速度的一个手段。
接着上面的步骤,有了图块以后,合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操做是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。
合成线程
运行方式以下👇
一般,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
相信你还记得,GPU 操做是运行在 GPU 进程中,若是栅格化操做使用了 GPU,那么最终生成位图的操做是在 GPU 中完成的,这就涉及到了跨进程操做。具体形式你能够参考下图:
从图中能够看出,渲染进程把生成图块的指令发送给 GPU,而后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。
栅格化操做完成后,合成线程会生成一个绘制命令,即"DrawQuad",并发送给浏览器进程。
浏览器进程中的viz组件
接收到这个命令,根据这个命令,把页面内容绘制到内存,也就是生成了页面,而后把这部份内存发送给显卡,那你确定对显卡的原理很好奇。
看了某博主对显示器显示图像的原理解释:
不管是 PC 显示器仍是手机屏幕,都有一个固定的刷新频率,通常是 60 HZ,即 60 帧,也就是一秒更新 60 张图片,一张图片停留的时间约为 16.7 ms。而每次更新的图片都来自显卡的前缓冲区。而显卡接收到浏览器进程传来的页面后,会合成相应的图像,并将图像保存到后缓冲区,而后系统自动将
前缓冲区
和后缓冲区
对换位置,如此循环更新。
这个时候,心中就有点概念了,好比某个动画大量占用内存时,浏览器生成图像的时候会变慢,图像传送给显卡就会不及时,而显示器仍是以不变的频率刷新,所以会出现卡顿,也就是明显的掉帧现象。
用一张图来总结👇
咱们把上面整个的渲染流水线,用一张图片更直观的表示👇
更新视图三种方式
另一个叫法是重排,回流触发的条件就是:对 DOM 结构的修改引起 DOM 几何尺寸变化的时候,会发生回流
过程。
具体一点,有如下的操做会触发回流:
width
、height
、padding
、margin
、left
、top
、border
等等, 这个很好理解。增减
或者移动
。offset
族、scroll
族和client
族属性的时候,浏览器为了获取这些值,须要进行回流操做。window.getComputedStyle
方法。一些经常使用且会致使回流的属性和方法:
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
scrollIntoView()
、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
依照上面的渲染流水线,触发回流的时候,若是 DOM 结构发生改变,则从新渲染 DOM 树,而后将后面的流程(包括主线程以外的任务)所有走一遍。
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color
、background-color
、visibility
等),浏览器会将新样式赋予给元素并从新绘制它,这个过程称为重绘。
根据概念,咱们知道因为没有致使 DOM 几何属性的变化,所以元素的位置信息不须要更新,从而省去布局的过程,流程以下:
跳过了布局树
和建图层树
,直接去绘制列表,而后在去分块,生成位图等一系列操做。
能够看到,重绘不必定致使回流,但回流必定发生了重绘。
还有一种状况:就是更改了一个既不要布局也不要绘制的属性,那么渲染引擎会跳过布局和绘制,直接执行后续的合成操做,这个过程就叫合成。
举个例子:好比使用CSS的transform来实现动画效果,避免了回流跟重绘,直接在非主线程中执行合成动画操做。显然这样子的效率更高,毕竟这个是在非主线程上合成的,没有占用主线程资源,另外也避开了布局和绘制两个子阶段,因此相对于重绘和重排,合成能大大提高绘制效率。
利用这一点好处:
提高合成层的最好方式是使用 CSS 的 will-change 属性
好比利用 CSS3 的transform
、opacity
、filter
这些属性就能够实现合成的效果,也就是你们常说的GPU加速。
非主线程
处理部分,即直接交给合成线程
处理。GPU
优点,合成线程生成位图的过程当中会调用线程池,并在其中使用GPU
进行加速生成,而GPU 是擅长处理位图数据的。createDocumentFragment
进行批量的 DOM 操做transform: translateZ(0);
你不知道的 Web Workers (上)[7.8K 字 | 多图预警]
How Browsers Work: Behind the scenes of modern web browsers
CSS GPU Animation: Doing It Right
本文使用 mdnice 排版