进程与线程
进程与线程分别是什么?
进程:进程是CPU资源分配的最小单位,是能拥有资源和独立运行的最小单位。
线程:线程是CPU调度的最小单位。一个进程中可以有多个线程,多个线程之间共享进程的资源。
我们也可以打开电脑的任务管理器,可以看到电脑各个进程占用的内存还有CPU利用率。
Chrome浏览器是多进程的
chorme浏览器有哪些进程?
打开谷歌浏览器的一个页面,再打开chrome的任务管理器(点击Chrome浏览器右上角的“选项”菜单,选择“更多工具”子菜单,点击“任务管理器”)可以看到浏览器的各个进程
浏览器的进程包括了:1个浏览器主进程、1个GPU进程、1个网络进程、多个渲染进程、多个插件进程
- 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU进程:其实,Chrome刚开始发布的时候是没有GPU进程的。而GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制,这使得GPU成为浏览器普遍的需求。最后,Chrome在其多进程架构上也引入了GPU进程。
- 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响
渲染进程包括哪些线程?
GUI渲染线程:负责渲染页面,解析HTML和CSS、构建DOM树、CSSOM树、渲染树和绘制页面,重排重绘也是在该线程执行
JS引擎线程:一个tab页中只有一个JS引擎线程(JS是单线程的),负责解析和执行JS。(JS引擎线程和GUI线程是互斥的,不能同时执行)
计时器线程:指的是setInterval和setTimeout,因为JS引擎是单线程的,所以如果处于阻塞状态,那么计数器就会不准的,所以需要单独的线程来负责计时器的工作。
异步http请求线程:XMLHttpRequest链接后浏览器开的一个进程,比如请求有回调函数,异步线程就会将回调函数加入事件队列,等待JS引擎空闲的时候执行
事件触发线程:主要用来控制事件循环,比如JS执行遇到定时器,ajax异步请求等,就会将对应任务添加到事件触发线程中,在对应事件符合触发条件时触发,就是把事件添加到待处理队列队尾,等JS引擎处理
渲染流程
当我们向浏览器输入HTML、CSS 、JavaScript代码,浏览器通过渲染就可以生成相应的页面。渲染按照时间顺序,可分为如下几个子阶段:构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。
构建DOM树
为什么要构建DOM树?
这是因为浏览器无法直接理解和使用HTML,所以需要将HTML转换为浏览器能够理解的结构——DOM树。
如何构建?
HTML 解析器是等整个 HTML 文档加载完成之后开始解析的,还是随着 HTML 文档边加载边解析的?
HTML 解析器并不是等整个文档加载完成之后再解析的,而是随着 HTML 文档边加载边解析的,是网络进程加载了多少数据,HTML 解析器便解析多少数据。
流程:网络进程接收到响应头之后,会根据响应头中的 content-type 字段来判断文件的类型,比如 content-type 的值是“text/html”,那么浏览器就会判断这是一个 HTML 类型的文件,根据这个判断选择相应的解析引擎,然后为该请求选择或者创建一个渲染进程。渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据传送给 HTML 解析器。
第一步:通过分词器将字节流转换为 Token。
分词器先将HTML字节流转换为一个个 Token,分为 Tag Token 和文本 Token。将 HTML 代码通过词法分析生成的 Token 如下图所示:
第二步:是将 Token 解析为 DOM 节点
HTML 解析器维护了一个Token 栈结构,该 Token 栈主要用来计算节点之间的父子关系,在第一个阶段中生成的 Token 会被按照顺序压到这个栈中。具体的处理规则如下所示:
- 如果压入到栈中的是StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。
- 如果分词器解析出来是文本 Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本 Token 是不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。
- 如果分词器解析出来的是EndTag 标签,比如是 EndTag div,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。
通过分词器产生的新 Token 就这样不停地压栈和出栈,整个解析过程就这样一直持续下去,直到分词器将所有字节流分词完成。
第三阶段是将 DOM 节点添加到 DOM 树中
将创建的 DOM 节点,添加到 document 上,形成 DOM 树。
样式计算
目的
样式计算的目的是为了计算出DOM节点中每个元素的具体样式。
过程
第一步:把CSS转换为浏览器能够理解的结构
浏览器无法直接理解代码所写的CSS样式,必须转换为浏览器能够识别的结构styleSheets
CSS样式来源主要有三种:
- 通过link引用的外部CSS文件
<style>
标记内的 CSS- 元素的style属性内嵌的CSS
可以在控制台输入document.styleSheets查看当前页面的styleSheets
第二步: 转换样式表中的属性值,使其标准化
标准化属性值就是将比如em、blue、bold,这种的转换为px、rgb(0,0,255)、700
第三步:计算出DOM树中每个节点的具体样式
样式的计算涉及到CSS的继承和层叠规则
样式的继承是如果子节点没有定义相应的样式,会继承父节点的样式,如果没有定义页面任何样式,会继承浏览器的默认样式(UserAgent)
样式的层叠就是处理多个地方定义了同个属性计算出最终属性值的算法
总结:
这个阶段就是将输入的CSS文本,经过以上3个步骤生成每个DOM节点的样式,并保存在ComputedStyle的结构内
布局阶段
布局是什么?
目前已经得到DOM树和DOM树中元素的样式,还需要知道DOM树中可见元素的几何位置才能得到页面,这就要进行布局。
过程
第一步,创建布局树
为什么要创建布局树?
因为DOM树还含有很多不可见的元素,比如head标签,还有使用了display:none属性的元素。所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树。
如何创建?
遍历DOM树中所有可见节点,并把节点加入布局树中,忽略不可见节点
第二步,布局计算
现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了。
在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容。
分层
什么是分层?
一些复杂的3D变换、页面滚动,或者使用z-indexing做z轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。
并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
过程
第一点,拥有层叠上下文属性的元素会被提升为单独的一层。
明确定位属性的元素、定义透明属性的元素、使用CSS滤镜的元素等,都拥有层叠上下文属性。
第二点,需要剪裁(clip)的地方也会被创建为图层。
什么是需要裁减的地方,就是比如说一个div里面包含了文字,这个div的大小是限定的,当文字超出这个大小时,这时候就产生了剪裁,渲染引擎会把裁剪文字内容的一部分用于显示在div区域,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。
图层绘制
渲染引擎实现图层的绘制就是把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表
如图,左边列表就是绘制列表
栅格化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系:
通常一个页面可能很大,用户可以看到的小部分叫做视口(viewport)。
在有些情况下,有的图层可以很大,视口只是一小部分,在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,基于这个原因,合成线程会将图层划分为图块(tile)
然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:
通常,栅格化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫快速栅格化,或者GPU栅格化,生成的位图被保存在GPU内存中。
相信你还记得,GPU操作是运行在GPU进程中,如果栅格化操作使用了GPU,那么最终生成位图的操作是在GPU中完成的,这就涉及到了跨进程操作。具体形式你可以参考下图:
从图中可以看出,渲染进程把生成图块的指令发送给GPU,然后在GPU中执行生成图块的位图,并保存在GPU的内存中。
合成和显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
到这里,经过这一系列的阶段,编写好的HTML、CSS、JavaScript等文件,经过浏览器就会显示出漂亮的页面了。
重排、重绘、直接合成
重排
从上图可以看出,如果你通过JavaScript或者CSS修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的
重绘
接下来,我们再来看看重绘,比如通过JavaScript更改某些元素的背景颜色,渲染流水线会怎样调整呢?你可以参考下图:
从图中可以看出,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
直接合成阶段
那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。具体流程参考下图
在上图中,我们使用了CSS的transform来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。
至于如何用这些概念去优化页面,我们会在后面相关章节做详细讲解的,这里你只需要先结合“渲染流水线”弄明白这三个概念及原理就行
优化
network时间线
优化时间线上耗时项
了解了时间线面板上的各项含义之后,我们就可以根据这个请求的时间线来实现相关的优化操作了。
1. 排队(Queuing)时间过久
排队时间过久,大概率是由浏览器为每个域名最多维护 6 个连接导致的。那么基于这个原因,你就可以让 1 个站点下面的资源放在多个域名下面,比如放到 3 个域名下面,这样就可以同时支持 18 个连接了,这种方案称为域名分片技术。除了域名分片技术外,我个人还建议你把站点升级到 HTTP2,因为 HTTP2 已经没有每个域名最多维护 6 个 TCP 连接的限制了。
2. 第一字节时间(TTFB)时间过久
这可能的原因有如下:
- 服务器生成页面数据的时间过久。对于动态网页来说,服务器收到用户打开一个页面的请求时,首先要从数据库中读取该页面需要的数据,然后把这些数据传入到模板中,模板渲染后,再返回给用户。服务器在处理这个数据的过程中,可能某个环节会出问题。
- 网络的原因。比如使用了低带宽的服务器,或者本来用的是电信的服务器,可联通的网络用户要来访问你的服务器,这样也会拖慢网速。
- 发送请求头时带上了多余的用户信息。比如一些不必要的 Cookie 信息,服务器接收到这些 Cookie 信息之后可能需要对每一项都做处理,这样就加大了服务器的处理时长。
对于这三种问题,你要有针对性地出一些解决方案。面对第一种服务器的问题,你可以想办法去提高服务器的处理速度,比如通过增加各种缓存的技术;针对第二种网络问题,你可以使用 CDN 来缓存一些静态文件;至于第三种,你在发送请求时就去尽可能地减少一些不必要的 Cookie 数据信息
3. Content Download 时间过久
如果单个请求的 Content Download 花费了大量时间,有可能是字节数太多的原因导致的。这时候你就需要减少文件大小,比如压缩、去掉源码中不必要的注释等方法。
JavaScript影响DOM树构建优化
https://juejin.cn/post/6992887065050349605#heading-6
预解析
CSS首次加载时白屏
https://juejin.cn/post/6844903981421068295#heading-5
- 通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
- 但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
- 还可以将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上 sync 或者 defer。
- 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。
- 通过以上策略就能缩短白屏展示的时长了,不过在实际项目中,总是存在各种各样的情况,这些策略并不能随心所欲地去引用,所以还需要结合实际情况来调整最佳方案。
css动画比JavaScript高效
https://juejin.cn/post/6901463548099067912
如果涉及到一些可以使用合成线程来处理 CSS 特效或者动画的情况,就尽量使用 will-change 来提前告诉渲染引擎,让它为该元素准备独立的层。但是凡事都有两面性,每当渲染引擎为一个元素准备一个独立层的时候,它占用的内存也会大大增加,因为从层树开始,后续每个阶段都会多一个层结构,这些都需要额外的内存,所以你需要恰当地使用 will-change如何系统优化加载阶段和交互阶段的页面
https://juejin.cn/post/7094516721708449805
在加载阶段,核心的优化原则是:优化关键资源的加载速度,减少关键资源的个数,降低关键资源的 RTT 次数。
在交互阶段,核心的优化原则是:尽量减少一帧的生成时间。可以通过减少单次 JavaScript 的执行时间、避免强制同步布局、避免布局抖动、尽量采用 CSS 的合成动画、避免频繁的垃圾回收等方式来减少一帧生成的时长
虚拟DOM和实际DOM有何不同
https://juejin.cn/post/6901465294158430222
PWA:解决了web应用哪些问题
https://juejin.cn/post/6901466694741393415