浏览器的进程/线程模型

区分进程和线程

  • 进程是一个工厂,工厂有它的独立资源 =》 系统分配的内存(独立的一块内存)
  • 工厂之间相互独立 -》 进程之间相互独立
  • 多个工人协作完成任务 -》 多个线程在进程中协作完成任务
  • 工厂内有一个或多个工人 -》 一个进程有一个或多个线程组成
  • 工人之间共享空间 -》 同一个进程下的各个线程之间共享程序的内存空间

进程是CPU资源分配的最小单位
线程是CPU调度的最小单位

浏览器是多进程程序

有一个主控进程,以及每个tab页面都会新开一个进程。
进程包括主控进程,插件进程,GPU,tab页面(浏览器内核)等等

  • Browser进程:浏览器的主进程(负责协调、主控),只有一个
  • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时创建
  • GPU进程,最多一个,用于3D绘制
  • 浏览器渲染进程(内核):默认每个tab页面一个进程,互不影响,控制页面渲染,脚本执行,事件处理等。

浏览器多进程的优势

  1. 避免单个page crash影响整个浏览器
  2. 避免第三方插件crash影响整个浏览器
  3. 多进程充分利用多核优势
  4. 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性

简单理解:如果浏览器是单进程,那么一个tab崩溃,就影响整个浏览器。

多线程的浏览器内核(渲染进程)—重点

渲染进程render process包含了以下多个主要线程:

  1. GUI线程
    1. 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
    2. 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
    3. 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  2. JS引擎线程
    1. 也称为JS内核(为渲染进程中的主线程 main thread),负责处理Javascript脚本程序。(例如V8引擎)
    2. JS引擎线程负责解析Javascript脚本,运行代码。
    3. JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序
    4. 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  3. 事件触发线程
    1. 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
    2. 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
    3. 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
    4. 由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
  4. 定时器线程
    1. 传说中的setInterval与setTimeout所在的线程
    2. 浏览器定时计数器并不是 有JavaScript引擎计数(因为JavaScript引擎是单线程,如果处于阻塞线程的状态就会影响计时的准确)
    3. 通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待js引擎空闲后执行)
    4. W3C在html标准中规定,规定要求setTimeout中低于4ms的实际间隔算为4ms
  5. 网络请求线程
    1. 在XMLHTTPRequest在连接后是通过浏览器新开的一个线程请求
    2. 在检测到状态变更时,如果有设置回调函数,异步线程就产生状态变更事件,将这个回调放入事件队列中。

      GUI渲染线程与JS引擎线程互斥

      由于JavaScript可操作DOM,如果在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致。为了方式渲染出现不可预期的结果,浏览器设置GUI渲染线程和JS引擎互斥的关系,当JS引擎执行时GUI线程会被挂起,GUI更新则会保存在一个队列中等到JS引擎线程空闲时立即被执行。

      JS阻塞页面加载

      根据以上描述的互斥关系,可以推导出,Js如果执行时间过长就会阻塞页面。
      假如js引擎正在进行巨量的计算,此时就算GUI有更新,也会被保存在队列中,等待JS引擎空闲后执行。
      由于巨量计算,所以JS引擎很可能很久才能空闲下来,自然会感觉到卡顿。所以要尽量避免JS执行时间过长,避免造成页面渲染不连贯,导致页面渲染加载阻塞的感觉。

image.png
蓝色线代表网络读取,红色线代表执行时间,这俩都是针对js脚本的;绿色线代表 HTML 解析。
此图告诉我们以下几个要点:

  1. deferasync 在网络读取(下载)这块儿是一样的,都是异步的(相较于 HTML 解析)
  2. 它俩的差别在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的
  3. 关于 defer,此图未尽之处在于它是按照加载顺序执行脚本的,这一点要善加利用
  4. async 则是一个乱序执行的主,反正对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行
  5. 仔细想想,async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的,最典型的例子:Google Analytics

WebWorker,JS的多线程

前文提到的JS引擎是单线程,而且JS执行时间过长会阻塞页面,那么js多cpu密集型计算怎么解决?后来html5中支持了WebWorker。
MDN的官方解释:

  • WebWorker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。
  • 每个worker是使用一个构造函数创建的一个对象运行一个命名的JavaScript文件
  • workers运行在另一个全局上下文中,不同于当前的window

总体概况:
创建Worker时,JS引擎向浏览器申请开一个子线程。JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)

输入URL后进行解析

URL一般包括几大部分

  • protocol:协议头,譬如有http、ftp
  • host:主机域名或IP地址
  • port:端口号
  • path:目录路径
  • query:查询参数
  • fragment:#后的hash值

    网络请求都是单独的线程

    每次网络请求都需要开辟单独的线程进行,譬如URL解析到http协议,会新建一个网络线程处理资源下载。,浏览器会根据解析得出协议,开辟一个网络线程,前往请求资源。

    DNS查询IP

    输入地址栏的是域名,需要进行dns解析IP,大致流程

  • 如果浏览器有缓存,直接使用浏览器缓存,否则使用本机缓存,再没有就使用host

  • 如果本地没有,就像dns域名服务器查询,查询对应的IP

在此过程dns解析很耗时,因此如果解析域名过多,会让首屏加载过慢,可以考虑dns-prefetch优化。

tcp/ip请求

http的本质是tcp/ip请求,需要在3次握手规则建立连接以及断开连接时的四次挥手,tcp将http长报文划分为短报文,通过三次握手与服务器建立连接,进行可靠传输。

三次握手的抽象比喻

  1. 客户端:hello,你是server吗?
  2. 服务端:hello,我是server,你是client吗?
  3. 客户端:也是,我是client

建立连接成功后,接下来可以传输数据

四次挥手的抽象比喻

  1. 主动方:我已经关闭了向你那边主动发消息的通道,只能被动接收
  2. 被动方:收到了通道关闭的信息
  3. 被动方:那我也告诉你,我这边向你主动的通道也关闭了
  4. 主动方:最后收到通道关闭数据,之后双方无法通信

    五层因特网协议(物数网传应)

    概念:从客户端发出的http请求到服务器接收,中间会经过一系列的流程。
    简括就是:从应用层的发送http请求,到传输层通过三次握手建立tcp/ip连接,再到网络层的ip寻址,再到数据链路层的封装成帧,最后到物理层的利用物理介质传输。
    服务端的接收是反过来的步骤:物数网传应(无数王传英)
    五层英特网协议其实是:1.应用层(dns、http)DNS解析成IP并发送http请求。2.传输层(tcp、udp)建立tcp连接(三次握手)3.网络层(IP,ARP)IP寻址。4.数据链路层封装成帧。5.物理层,物理传输(传输的时候通过双绞线,电磁波等介质)
    OSI七层框架
    在应用层又细分出会话层,表示层
    OSI七层框架:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层

前台和后台http交互

通用头部

  • Request Url:请求的web服务器地址
  • Request Method:请求方式(GET,POST,OPTIONS,PUT,HEAD,DELETE、CONNECT、TRACE)
  • Status Code:请求的返回状态码
  • Remote Address:请求的远程服务器地址

Mehod方式类型

  • http1.0定义了三种请求方法,GET、POST、和HEAD方法
  • http1.1新增五种请求方法:OPTIONS,PUT,DELETE,TRACE和CONNECT方法

    常用请求头部 Request

  • Accept:接收类型,表示浏览器支持的MIME类型

  • Accept-Encoding:浏览器支持的压缩类型,如gzip等,超出类型不能接收
  • Content-type:客户端发送出去实体内容的类型
  • Cache-Control:指定请求和响应遵循的缓存机制,如no-cache
  • If-Modified-Since:对应服务端的Last-Modified,用来匹配文件是否变动,只能精准到1s内,http1.0
  • Expires:缓存控制,在这个时间内不会请求,直接使用缓存,http1.0,而且是服务端时间。
  • Max-age:代表资源在本地缓存多少秒,有效时间内不会请求,而是使用缓存,http1.1中
  • If-None-Match:对应服务端的ETag,用来匹配文件内容是否改变(非常精确),http1.1中
  • Cookie:有cookie并且同域访问时会自动带上
  • Connect:当浏览器与服务器通信时,对于长连接如何进行处理,如keep-alive
  • Host:请求的服务器URL
  • Origin:最初的请求是从哪里发出的,Origin比Referer更尊重隐私
  • Referer:该页面的来源URL
  • User-Agent:用户客户端的一些必要信息,如UA头部

    常用的响应头部 Response

  • Access-Control-Allow-Headers:服务端允许的 请求headers

  • Access-Control-Allow-Methods:服务端允许的请求方法
  • Access-Control-Allow-Origin:服务端允许的请求Origin头部(如*)
  • Content-Type:服务端返回的实体内容类型
  • Date:数据从服务器发送的时间
  • Cache-Control:告诉浏览器,什么环境可以安全的缓存文档
  • Last-Modified:请求资源的而最后修改时间
  • Expires:应该在什么时候认为文档已经过期,从而不在缓存它
  • Max-age:客户端本地资源应该缓存多少秒,开启了Cache-Control后有效
  • Etag:请求变量的实体标签的当前值
  • Set-Cookie:设置和页面管理的Cookie,服务器通过头部把cookie传递给客户端
  • Keep-alive:如果客户端有keep-alive,服务端也会有响应

    浏览器渲染render

    微信截图_20210123213128.png
    浏览器的eventLoop执行
    js分为heap堆,和stack栈。heap中存放引用的对象,stack调用栈,存放要执行的任务的引用。
    stack中的任务是先进后出,queue中的任务是先进先出。
    queue中的任务要等stack任务执行完毕,才会推入一个queue进入stack。
    stack中的异步任务会被放到webAPIs中,当webapis执行完成进入到queue中。

浏览器关键渲染路径

浏览器渲染流程.jpg
浏览器中一帧的执行顺序是,先执行js,然后对style样式即css解析,之后是layout分层(对dom结构的分层处理)。paint是重绘,最后一步是composite合成。
3963958-432f5165ba423f57.png
通过上图可看到,一帧内需要完成如下六个步骤的任务:

  • 处理用户的交互
  • JS 解析执行
  • 帧开始。窗口尺寸变更,页面滚去等的处理
  • requestAnimationFrame(rAF)
  • 布局
  • 绘制

在一帧中,requestAnimationFrame是每次都会执行,执行时间是在第四阶段。浏览器的一帧一般是16.666ms,js执行的任务有可能用不了这么久,就会有剩余时间,此时剩余时间就会执行requestIdleCallback里面注注册的任务。
3963958-01ac3e74fd8c0acf.png
requestIdleCallback并不能确保每帧都会执行。

浏览器渲染流程:

  • 浏览器输入URL,浏览器主进程(Browser process)接管,开始(在UI thread内)下载线程
  • 然后(network thread)进行http请求(DNS查询、IP寻址),然后等待响应,获取内容
  • (UI thread)将内容通过RendererHost接口转交给Renderer进程(render process)
  • 浏览器渲染流程开始

浏览器内核拿到内容后,开始渲染的几个步骤

  1. 解析html建立DOM树
  2. 解析css构建cssom树(将css代码解析成树形的数据结构,然后结合DOM合并成render树)
  3. 布局render树(Layout/reflow),负责个元素尺寸、位置的计算
  4. 绘制render树(paint),绘制页面像素信息
  5. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。

浏览器渲染完毕后就是load事件,之后是处理js逻辑处理。

Load事件与DOMContentLoaded事件的先后

渲染完毕后触发load事件,那么load事件与DOMContentLoaded事件谁先后?

  • DOMContentLoaded事件触发时,仅当DOM加载完成,不包括样式表,图片。如果有async加载的脚本就不一定加载完成
  • 当onload事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成。

    css加载是否会阻塞dom树渲染

    头部引入css的情况,css是单独的下载线程异步下载的

  • css加载不会阻塞DOM树解析(异步加载时DOM照常构建)

  • 但是会阻塞render树渲染(渲染是需等css加载完毕,因为render树需要css信息)

加载css的时候,可能会修改下面DOM节点的样式, 如果css加载不阻塞render树渲染的话,那么当css加载完之后, render树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。 所以干脆就先把DOM树的结构先解析完,把可以做的工作做完,然后等你css加载完之后解析出cssom,在根据最终的样式来渲染render树,这种做法性能方面确实会比较好一点。

浏览器渲染的图层:普通和复合 图层

渲染步骤中提到composite概念,浏览器渲染的图层包含两大类:普通图层和复合图层
普通文档流内是一个复合图层(默认复合层),里面不管有多少元素,其实都是在同一个复合图层中。absolute(fixed)布局,虽然可以脱离文档流,但仍属于默认复合层

可以通过硬件加速的方法,声明一个新的复合图层,它会单独分配资源,新的复合图层不管怎么变换,都不会影响到默认复合图层里的回流重绘。
GPU中,各个复合图层是单独绘制的,互不影响。可以在Chrome源码调试 -》 More Tools -》 Rendering -》 Layerborders中看到,黄色的就是复合图层信息

如何变成复合图层

使用3D加速,translate3d和translateZ

absolute和硬件加速

absolute虽然脱离普通文档流,但是无法脱离默认的复合层。absolute中信息改变时不会改变普通文档流中render树, 但是,浏览器最终绘制时,是整个复合层绘制的,所以absolute中信息的改变,仍然会影响整个复合层的绘制。
而硬件加速直接在另一个复合层,所以它的信息改变不会影响默认复合层,仅仅引发最后的合成。

复合图层的作用

元素开启硬件加速后变成复合图层,可以独立于普通文档流,改动后可以避免整个页面重绘,提升性能,但是尽量不要大量使用复合图层,否则会由于自由消耗过度,页面反而变得更卡。

浏览器http发展阶段

http0.9

最初在1991发布的,只能传输简单的超文本。HTTP 都是基于 TCP 协议的,所以客户端先要根据 IP 地址、端口和服务器建立TCP 连接,而建立连接的过程就是 TCP 协议三次握手的过程。
当时的请求简单,返回内容简单,传输的数据以 ASCII 字符流来传输的。

http1.0

加入请求头、响应头内容,可传输多种格式的数据
请求头内容如下

  1. accept: text/html
  2. accept-encoding: gzip, deflate, br
  3. accept-Charset: ISO-8859-1,utf-8
  4. accept-language: zh-CN,zh

响应头数据

  1. content-encoding: br
  2. content-type: text/html; charset=UTF-8

http1.1

默认开启keep-alive 链接复用,使用管线化,一次可以建立6个tcp连接,服务器处理多个请求(存在问题:队头阻塞)。
在tcp传输中,如果存在单个数据包丢失,就会阻塞后续的传输,称为TCP的队头阻塞。
相比http1.0的提升

  1. 改进持久连接
  2. HTTP/1.1 中试图通过管线化的技术来解决队头阻塞的问题。HTTP/1.1 中的管线化是指将多个 HTTP 请求整批提交给服务器的技术,虽然可以整批发送请求,不过服务器依然需要根据请求顺序来回复浏览器的请求。
  3. 提供虚拟主机支持
  4. 对动态内容生成提供完美支持
  5. 客户端Cookie,安全机制

    http2.0

    新特性

    使用同一个tcp连接发送数据,一个域名一个tcp(多路复用,采用二进制分帧技术,将数据加上)
    头部压缩,
    服务器可推送数据给客户端,
    资源优先级发送传输。

    多路复用

    HTTP/2 最核心、最重要且最具颠覆性的多路复用机制。每个请求都有一个对应的 ID,如 stream1 表示 index.html 的请求,stream2 表示 foo.css 的请求。这样在浏览器端,就可以随时将请求发送给服务器,服务器端接收到这些请求后,会根据自己的喜好来决定优先返回哪些内容。
    HTTP/2 使用了多路复用技术,可以将请求分成一帧一帧的数据去传输,这样带来了一个额外的好处,就是当收到一个优先级高的请求时,比如接收到 JavaScript 或者 CSS 关键资源的请求,服务器可以暂停之前的请求来优先处理关键资源的请求。
    image.png

    TCP 丢包

    TCP 传输过程中,由于单个数据包的丢失而造成的阻塞称为 TCP 上的队头阻塞。
    那队头阻塞是怎么影响 HTTP/2 传输的呢?首先我们来看正常情况下 HTTP/2 是怎么传输多路请求的,为了直观理解可以参考下图
    image.png

    多路复用也存在问题

    在 HTTP/2 中,多个请求是跑在同一个 TCP 管道中的,如果其中任意一路数据流中出现了丢包的情况,那么就会阻塞该 TCP 连接中的所有请求。这不同于HTTP/1.1,使用 HTTP/1.1 时,浏览器为每个域名开启了 6 个 TCP 连接,如果其中的 1
    个 TCP 连接发生了队头阻塞,那么其他的 5 个连接依然可以继续传输数据。
    所以随着丢包率的增加,HTTP/2 的传输效率也会越来越差。有测试数据表明,当系统达到了 2% 的丢包率时,HTTP/1.1 的传输效率反而比 HTTP/2 表现得更好。

    http3.0

    采用UDP的QUIC协议,解决了tcp的队头阻塞问题
    QUIC 实现了在同一物理连接上可以 有多个独立的逻辑数据流,实现了数据流的单独传输,就解决了 TCP 中队头 阻塞的问题。
    image.png

    QUIC 协议的多路复用

    由于 QUIC 是基于 UDP 的,所以 QUIC 可以实现使用 0-RTT 或 者 1-RTT 来建立连接,这意味着 QUIC 可以用最快的速度来发送和接收数据,这样可以 大大提升首次打开页面的速度。

    QUIC 协议数据安全

    HTTP/3 选择了一个折衷的方法——UDP 协议,基于 UDP 实现了类似于 TCP 的 多路数据流、传输可靠性等功能,我们把这套功能称为 QUIC 协议。关于 HTTP/2 和 HTTP/3 协议栈的比较,你可以参考下图
    image.png
    HTTP/3 中的 QUIC 协议集合了以下几点功能

  6. 实现了类似 TCP 的流量控制、传输可靠性的功能。虽然 UDP 不提供可靠性的传输,但 QUIC 在 UDP 的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥 塞控制以及其他一些 TCP 中存在的特性。

  7. 集成了 TLS 加密功能。
  8. 实现了数据流的单独传输,真正的多路复用

    JavaScript运行机制

    可执行上下文

VO/Scop chain

从Event Loop谈JS的运行机制

JS分为同步任务和异步任务,同步任务在主线程上执行,形成一个执行栈。主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列中放置一个事件。一旦执行栈中所有的同步任务执行完毕(此时JS引擎空闲),系统就会读取异步任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
微信截图_20200911175441.png

为什么setTimeout推入的事件不能准时执行?

因为可能在它推入到事件列表时,主线程还不空闲,正在执行其他代码,所以会有误差。

单独说说定时器

事件循环机制的核心是:JS引擎线程和事件触发线程。但事件上,里面还隐藏些细节,譬如setTimeout后是如何等待特定事件后添加到事件队列中的?是js引擎线程控制的吗?当然不是!它是有定时器线程控制。

为什么要单独的定时器线程?

因为JavaScript引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确,因此很有必要单独开一个线程用来计时。

什么时候用到定时器线程?

当使用setTimeout和setInterval时,它需要定时器线程计时,计时完成后就会将特定的事件推入事件队列。

setTimeout而不是setInterval

用setTimeout模拟定期计时和直接用setInterval是有区别的
每次setTimeout计时到点后就会去执行,然后执行一段时间后才会继续setTimeout,中间有误差。而setInterval则是每次都精准的隔一段时间推入一个事件(事实上事件的实际执行时间不一定准确,还有可能是这个事件没有执行完毕,下一个事件就来了)

setInterval致命的问题

  • 累计效应,如果setInterval代码再次添加到队列之前上次任务还没有完成执行,就会导致定时器内代码连续运行多次,而且之间没有间隔,就算正常间隔执行,多个setInterval的代码执行时间可能会比预期的少。
  • 在IOS的webview或者safari等浏览器中都有一个特点,在滚动的时候是不执行js的,如果使用setInterval会发现在滚动结束后会执行多次由于滚动不执行js累计的回调,如果回调执行时间过长,就会造成卡顿问题和一些不可知的错误
  • 把浏览器最小化显示等操作后,setInterval并不是不执行程序,这些操作会把setInterval的回调函数放在队列中,等浏览器窗口再次打开时,一瞬间全部执行。

鉴于这么多但问题,目前一般认为的最佳方案是:用setTimeout模拟setInterval,或者特殊场合直接用requestAnimationFrame

事件循环进阶macrotask宏任务和microtask微任务

具体详细解读可以看
执行流程。
文章将js事件循环机制详细解读一遍。
JS中分为两种任务类型:macrotask宏任务 和 microtask微任务。在ECMAScript中,microtask被称为jobs,macrotask被称为task。
eventLoop.png

宏任务的特性

  • macrotask宏任务是每次执行栈 执行的代码就是一个宏任务(每次从事件队列中获取一个事件回调并放到执行栈中执行)
  • 每一个task会从头到尾将这个任务执行完毕,不执行其他
  • 浏览器为了能够使得js内部task与DOM任务有序的执行,会在一个task执行结束后,下一个task执行开始前,对页面进行重新渲染。

    微任务特性

  • 在当前task任务后,下一个task之前,在渲染之前

  • 微任务响应速度比setTimeout会更快,无需等渲染
  • 在某一个宏任务执行完后,就会将在它执行期间产生的所有微任务都执行完毕

    macrotask宏任务和microtask微任务场景

  • macrotask:主代码块(script),setTimeout,setInterval等

  • microtask:Promise,process.nextTick(node下)、postMesssage(window)

任务的执行顺序:
宏任务 > 当前宏任务下的同步任务 > 当前宏任务下的微任务 > 当前宏任务执行完成 > 渲染页面 > 执行下一次宏任务 > ……