1、浏览器内部组成
我们看到浏览器主要包括:
- 1个浏览器主进程: 主要负责界面显示,用户交互,子进程管理
- 多个渲染进程:一般浏览器会为每个Tab标签窗口创建一个渲染进程,主要负责将html,css,JavaScript转换成我们看到的网页,里面包含多个线程,比如JavaScript的V8引擎。
- 1个GPU进程: 主要负责复杂的计算,比如3D动画,图形绘制。
- 1个网络进程: 主要负责网络资源加载
- 多个插件进程: 浏览器器每个插件都会分配一个插件进程。
2、从一个url开始
我们下面来看在地址栏输入一个url后,浏览器做了什么事,我们先来看下流程图:
下面我们来分析下流程图:
- 当用户在地址栏输入一个地址或者关键字,并按下回车键的时候,意味着当前页面很快要被替换,在这个时候会触发当前页面的beforeunload事件。
- 然后浏览器的当前tab栏就变成加载状态,变成一个转动的圆圈,此时页面还没有开始改变,需要等到后面"提交文档"后,才会别新内容替换。
- 浏览器主进程合成完整Url:如果是输入的是地址,比如"baidu.com",则自动合成为:https://www.baidu.com/。
如果输入的是关键字,则使用默认搜索引擎,合成带搜索关键字Url,比如输入:'hello',默认搜索引擎为百度,则合成为:https://www.baidu.com/s?ie=UTF-8\&wd=hello,然后把完整url发送给网络进程。 - 网络进程接收到url请求后,先判断是否本地缓存了资源。如果有,则直接返回资源给浏览器主进程,不发起网络请求。如果没有缓存,则进入网络请求。
- 网络请求之前,先要进行DNS解析,把域名转换成ip,这一步也是先查DNS缓存,如果有当前域名的缓存,则从缓存中直接取对应ip。如果没有缓存,则从DMS服务器请求ip。然后构建请求体,请求头(包括cookie)等信息,向服务端发送网络请求(建立Tcp链接)。
- 服务端接收到请求消息后,进行对应操作,然后生成响应数据,发送给网络进程。
- 网络进程接收到服务器返回的响应数据后,先解析响应头信息,判断状态码是否为重定向(3xx),如果是,则取响应头中Location字段,重新发起请求。如状态码为200,表示请求成功,可以继续处理请求。
- 如果状态码为200,浏览器主进程会根据响应头中的Content-Type字段做出响应对策,如果此字段的值为application/octet-stream,则启动下载流程。如果Content-Type为text/html,则启动渲染流程。
- 默认情况下,浏览器会为每一个tab页签创建一个渲染进程,但是如果是同一个站点(根域名+协议相同,端口+子域名不同),则共用一个渲染进程。
- 进入渲染流程开始前,浏览器主进程会发送一个"接收文档"消息给渲染进程,这里的文档是指存在网络进程里面的响应体信息。
- 渲染进程接收到"提交文档"的消息后,会和网络进程建立一个通道,接收数据。
- 渲染进程接收到数据后,开始向浏览器主进程发送"确认提交",消息
- 浏览器主进程接收到"确认提交"的消息后,开始更新浏览器页面,包括:地址栏的url,前进后退按钮。
- 渲染进程开始生成页面,这个过程是一边接收一边生成。当页面渲染完毕后(当前页面及内部iframe都出发了onload事件),发送"渲染完毕"消息。
- 浏览器主进程接收到消息后,显示页面,并停止标签栏的加载动画。
到这里为止,当我们在地址栏输入一个url,然后到页面展示在我们面前的大致流程就梳理完毕了。但是这里面还有一个非常重要的环节,就是页面解析的流程我们上面只是一带而过,这是渲染进程来做的工作,下面来具体展开。
3、渲染进程
渲染进程的核心工作就是解析接收到的html/js/css代码,并将其转换成用户可交互的页面。
渲染进成包含:
- 主线程GUI:负责解析dom结构
- js引擎线程:负责执行js代码,会阻塞主进程。
- 合成线程:分组,合成,并把视口附近图块提交给光栅化线程。
- 多个光栅化线程:生成位图,即页面需要的每个像素点的颜色值(我们看到的页面其实就是每个像素点的颜色)
下面来分析下流程图:
- 渲染进程开始接受到数据的时候,为了提高效率,会先预扫描接收到的数据,如果如果发现有需要加载资源的标签(img,link,外部script等),就先告诉浏览器主进程,先去下载,这个过程叫预解析,这个任务交出去后,就继续做自己本职工作,解析html文件。
当主线程解析html文件时,会碰到三种类型数据:html标签,css代码,js代码。
html标签:对于普通的html标签,会生成Dom树(标签节点的结构树,是浏览器的内置对象,会有一些内置方法和属性)。
css样式:对于css代码,会根据css的样式选择器构建cssDom树,并对样式进行计算(rem,em转换为px,没有定义样式的提供默认样式),生成computedStyle。
如果遇到的是css外部链接,如果从预解析开始还没下载完,则继续下载,不会阻塞解析。
js代码:对于js代码,会先判断js代码前的css有没有解析完(包括外部css的下载),如果没有则等待css代码下载完并解析完毕,然后再执行js代码。js执行期间阻塞解析。所以步骤是这样:遇到js -> 阻塞dom树构建 -> css下载 -> css解析->js执行->继续构建dom树
js链接:对于js的链接,如果标签上没有设置异步标志(async/defer),则和普通的js代码一样,下载也会阻塞dom解析,也需要等css下载解析完,但是css下载不会阻塞js下载,步骤如下:
遇到js链接(无异步标签) -> 阻塞dom树构建 -> css下载(同时js下载) -> css解析->js执行->继续构建dom树如果有异步标签,则下载不阻塞dom树构建,async文件下载完,立即执行。defer文件下载完,等html解析完,按加载顺序执行。步骤如下:
遇到js链接(async) ->下载js(不影响dom构建) -> js下载完毕 -> 立即执行js(走普通js代码流程)
遇到js链接(defer) ->下载js(不影响dom构建) -> js下载完毕 -> 等html解析完毕 -> 按顺序执行js
- 等dom树和computedStyle都构建完毕后(要都构建完毕),
更具dom树和computedStyle,构建布局树layoutTree,布局树包含每个节点的位置坐标和盒模型的大小,并且剔除了隐藏的节点(样式设置了display:none的节点)。 - 等布局树layoutTree构建完毕后,我们已经知道了页面上要显示的每个节点的大小,位置和样式。继续来主线程会对节点进行分层,通过遍历layoutTree构建图层树layerTree。哪些节点会被分为一层呢?分为两种情况:
拥有层叠上下文属性的元素会被单独提升为一层(什么是层叠上下文),包含设置了z-index,transform,will-change,filter,opacity<1,flex子元素等等。
需要裁剪的地方会被分为一层,即元素的大小被限制,而内容超出元素大小,内容被裁剪。
- 图层树layerTree被创建后,会为每一个图层创建绘制指令列表,可以再浏览器调试窗口的layers标签下查看分层和指令列表信息。渲染进程的主线程把绘制指令生成后,并不执行,而是转交给合成线程。
- 合成线程先把图层分为图块(大小通常为256256/512512),然后把浏览器用户视口附近的图块优先交给栅格化线程来生成位图。
- 栅格化的最小执行单位是图块,即最少要把一个图块栅格化。栅格化的过程通常会用GPU执行,就是说栅格化线程会把绘制图块的指令发送给GPU,然后GPU生成图块的位图(像素点的颜色值),存在GPU内存。
- 当视口附近所有图块栅格化完毕后,合成线程发送DrawQuad指令给浏览器主进程,浏览器主进程把页面的内容显示在屏幕上。