浏览器中的进程:
Browser进程:是浏览器的主进程,只有一个,负责主控,协调,可以看做是浏览器的大脑
功能:
- 负责下载页面的网络文件
- 负责将Render进程得到的存在内存中的位图渲染到页面上
- 负责创建的销毁tab进程(Render进程)
- 负责与用户交互
GPU进程:只有一个
功能:
- 负责3D绘制,只有当该页面使用了硬件加速才会使用它,来渲染页面,否则的话,不适用该进程,而是使用Browser进程来渲染页面
Render进程:又名浏览器内核,每个tab页面对应一个独立的Render进程,内部有多个线程
功能:
- 负责脚本执行,位图绘制,事件触发,任务队列轮询等
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
浏览器是多进程的好处非常明显,如果浏览器是单线程的话,则一个页面,一个插件的崩溃会导致整个浏览器崩溃,用户体验感会非常差。
最主要的是浏览器内核:Render进程(渲染进程)
Render进程具有多个线程:
js引擎线程:
- 也称js内核,解析js脚本,执行代码;
- 与GUI线程互斥,即当js引擎线程运行时,GUI线程会被挂起,当js引擎线程结束运行时,才会继续运行GUI线程
- 由一个主线程和多个web worker线程组成,由于web worker线程时附属于主线程,无法操作dom等,所以js还是单线程语言(在主线程中运行js代码)
GUI渲染线程:
- 用于解析html为DOM树,解析css为CSSOM树,布局layout,绘制paint
- 当页面需要重排reflow,重绘repaint时,使用该线程
- 与js引擎线程互斥
事件触发线程:
- 当对应事件触发(不管是Web接口完成事件触发,还是页面交互事件触发)时,该线程都会将事件对应的回调函数放入callback queue(任务队列)中,等待js引擎线程的处理
定时触发线程:
- 对应于setTimeout,setInterval,由该线程来计时,当计时结束,将事件对应的回调函数放入任务队列,等待js引擎线程的处理
- 当定时的时间小于4ms时,一律按4ms计算
http请求线程:
- 每有一个http请求久开一个http请求线程
- 当检测到http的状态发生改变,就会产生一个状态变更事件,如果该事件对应有回调函数的话,则放入任务队列中
任务队列轮询线程:
- 用于轮询监听任务队列,以知道任务队列是否为空
浏览器进程间的通信过程
- Browser进程收到用户请求,首先需要获取页面内容(譬如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程
- Render进程的Render接口收到消息,简单解释后,交给渲染线程,然后开始渲染
- 渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
- 当然可能会有JS线程操作DOM(这样可能会造成回流并重绘)
- 最后Render进程将结果传递给Browser进程
- Browser进程接收到结果并将结果绘制出来
Render进程(渲染进程/浏览器内核)中线程间的关系
GUI渲染线程和JS引擎线程互斥
由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起, GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。
JS阻塞页面加载
从上述的互斥关系,可以推导出,JS如果执行时间过长就会阻塞页面。
譬如,假设JS引擎正在进行巨量的计算,此时就算GUI有更新,也会被保存到队列中,等待JS引擎空闲后执行。 然后,由于巨量计算,所以JS引擎很可能很久很久后才能空闲,自然会感觉到巨卡无比。
所以,要尽量避免JS执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
WebWorker,JS的多线程
前文中有提到JS引擎是单线程的,而且JS执行时间过长会阻塞页面,那么JS就真的对cpu密集型计算无能为力么?
所以,后来HTML5中支持了Web Worker
。
MDN的官方解释是:
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面 一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的JavaScript文件 这个文件包含将在工作线程中运行的代码; workers 运行在另一个全局上下文中,不同于当前的window 因此,在一个 Worker 内使用 window快捷方式获取当前全局的范围 (而不是self) 将返回错误。
这样理解下:
- 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
- JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
所以,如果有非常耗时的工作,请单独开一个Worker线程,这样里面不管如何翻天覆地都不会影响JS引擎主线程, 只待计算出结果后,将结果通信给主线程即可,perfect!
而且注意下,JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。
WebWorker和SharedWorker
WebWorker只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享
- 所以Chrome在Render进程中(每一个Tab页就是一个render进程)创建一个新的线程来运行Worker中的JavaScript程序。
SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用
- 所以Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中每个相同的JavaScript只存在一个SharedWorker进程,不管它被创建多少次。
看到这里,应该就很容易明白了,本质上就是进程和线程的区别。SharedWorker由独立的进程管理,WebWorker只是属于render进程下的一个线程。
浏览器的渲染过程
浏览器渲染的过程主要包括以下五步:
- 浏览器将获取的HTML文档解析成DOM树。
- 处理CSS标记,构成层叠样式表模型CSSOM(CSS Object Model)。
- 将DOM和CSSOM合并为渲染树(
rendering tree
),代表一系列将被渲染的对象。 - 渲染树的每个元素包含的内容都是计算过的,它被称之为布局
layout
。浏览器使用一种流式处理的方法,只需要一次绘制操作就可以布局所有的元素。 - 将渲染树的各个节点绘制到屏幕上,这一步被称为绘制
painting
。
需要注意的是,以上五个步骤并不一定一次性顺序完成,比如DOM或CSSOM被修改时,亦或是哪个过程会重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。而在实际情况中,JavaScript和CSS的某些操作往往会多次修改DOM或者CSSOM。
渲染过程中的问题
DOMContentLoaded和Load
用户看到页面实际上可以分为两个阶段:页面内容加载完成和页面资源加载完成,分别对应于DOMContentLoaded
和Load
。
DOMContentLoaded
事件触发时,仅当DOM加载完成,不包括样式表,图片等load
事件触发时,页面上所有的DOM,样式表,脚本,图片都已加载完成- DOMContentLoaded -> load
渲染阻塞
JS可以操作DOM来修改DOM结构,可以操作CSSOM来修改节点样式,这就导致了浏览器在遇到<script>
标签时,DOM构建将暂停,直至脚本完成执行,然后继续构建DOM。如果脚本是外部的,会等待脚本下载完毕,再继续解析文档。
每次去执行JavaScript脚本都会严重地阻塞DOM树的构建,如果JavaScript脚本还操作了CSSOM,而正好这个CSSOM还没有下载和构建,浏览器甚至会延迟脚本执行和构建DOM,直至完成其CSSOM的下载和构建。所以,script
标签的位置很重要。
<script src="script.js"></script>
- 没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。
<script defer src="script.js"></script>
(延迟执行)
- defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。
- defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。
<script async src="script.js"></script>
(异步下载)
- 加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
- async 属性表示异步执行引入的 JavaScript
浏览器的回流(reflow)和重绘(repaint)
HTML默认是流式布局的,但CSS和JS会打破这种布局,改变DOM的外观样式以及大小和位置。因此我们就需要知道两个概念:replaint
和reflow
。
回流(reflow)
当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的 DOM 元素
- 激活 CSS 伪类(例如::hover)
- 查询某些属性或调用某些方法
重绘(repaint)
当页面中样式发生改变(背景色 ,颜色,字体改变(字体大小改变会发生回流)等),把这些改变不会引起页面布局的变化的时候,浏览器就只会把新样式赋予元素并重新绘制,这个过程就叫重绘。
如何避免回流
CSS
- 避免使用 table 布局。
- 尽可能在 DOM 树的最末端改变 class。
- 避免设置多层内联样式。
- 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上。
- 避免使用 CSS 表达式(例如:calc())。
Javascript
- 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。
- 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。
- 也可以先为元素设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
参考资料:
深入前端-彻底搞懂浏览器运行机制
从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理
浏览器渲染原理与过程
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。