1.浏览器内部组成
我们先来看浏览器的内部组成(以chrome为例):
我们看到浏览器主要包括:
-
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
- html标签:对于普通的html标签,会
- 等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指令给浏览器主进程,浏览器主进程把页面的内容显示在屏幕上。
4.应用
那么知道了浏览器的基本原理后,对我们开发有什么实际的作用呢?以下总结了几点:
- css会阻塞js,js会阻塞dom解析,所以尽量把css文件放页面上面,js放在页面下面。
- 对于不会影响页面内容的js外部文件,可以用async/defer标记来异步加载。
- css动画效率比js操作dom实现动画好,因为css动画的只会引起
合成
及以后步骤的重新执行。而合成步骤是在合成线程
,不会阻塞渲染的主线程。而js如果影响到dom节点的大小样式位置,则需要触发布局
及以后的步骤。
参考: