【浏览器】浏览器基本工作原理

时间:2024-02-01 11:57:15

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
  • 等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节点的大小样式位置,则需要触发布局及以后的步骤。

参考: