写点什么

【万字爆肝】带你了解浏览器原理

作者:FE情报局
  • 2022-11-29
    陕西
  • 本文字数:9883 字

    阅读完需:约 32 分钟

【万字爆肝】带你了解浏览器原理

背景

为什么要了解浏览器原理?

当面试官问你输入 url 到渲染发生了什么这种问题你不知所措?

页面中到底能承载多少个元素,取决于什么条件?如果一个页面在 2s 内打不开,你应该如何优化?

DOM 是什么,javascript 操作的是 DOM 还是 html?

回流和重绘又是什么?浏览器架构是什么样的?

当你能够细化的了解整个了浏览器工作原理的时候,你就能很好的处理这些问题

到底什么是浏览器

浏览器我们常用的有谷歌 IE Safari 火狐等等,目前开发者心中的浏览器只有一个,那就是谷歌浏览器,它的市场份额稳居第一,从未被超越


以工程师的维度,或者开发者的维度怎么看浏览器,它是一套标准,这套标准可以运行 html、css、javascript 代码,这些内容可以通过超文本传输协议进行传输,通过浏览器的标准进行展现,能够让世界上任何使用浏览器的人都能够看到网页内容

页面的标准有 W3C,语言标准有 ECMAScript,当然还有网络标准,等各种标准由浏览器统一管理。这些标准共同作用,即便是不同的浏览器,它们的标准也是一致的。当然并不是完全一致,一些浏览器还是有自己的想法的,就是这些想法给前端程序员带来了巨大的工作量,比如工程师们闻之色变的兼容 IE,主要原因就是 IE 浏览器有自己的想法,搞了很多和别人不太一样的东西,导致同样的代码和逻辑,在 IE 浏览器上展示就有问题


线程和进程

在深入研究浏览器架构之前要掌握的另一个概念是进程和线程。通常一个好的应用程序会把自己划分为几个独立的模块和几个相互配合的模块,浏览器本身也是这样,谷歌浏览器为例,它由多个线程和多个进程组成,进程之间相互协作完成浏览器整体的大功能,进程又包含很多线程,一个进程内的线程也会相互配合完成这个进程的工作职责

对于前端同学来说,这些概念是模糊的,概念上讲什么进程是资源分配的最小单位,线程是 CPU 调度的最小单位,很多同学听了可能还是有些懵懂,我们用图来简单讲述一下

以工厂为例,我们可以把工厂理解成进程,工人理解成线程,工人只能在工厂工作,一个工厂可以有很多工人,同一个工厂内的工人很容易交流,不同的工厂内的工人不容易交流,只是会比较费劲,一个工厂不会影响另一个工厂,但是工厂内的工人会影响这个工厂的运行

当启动一个应用程序时,会创建一个进程或者多个进程,该程序可能会创建线程来帮助它工作。操作系统为每个进程提供了可用的内存,所有应用程序状态都保存在内存空间中。当您关闭应用程序时,程序创建的进程也会消失,占有的内存也会被释放

chrome 架构

了解了进程和线程的关系之后,我们可以看一下启动 chrome 浏览器需要占用多少进程

多进程架构

谷歌浏览器自带了一个任务管理器,点击浏览器【更多工具】→【任务管理器】


可以看到对应的浏览器进程


各进程作用

这张图其实就表明浏览器其实是多进程的架构,当然这是不断演化的结果,并不是一蹴而就的,简单来看一下这些进程的作用

  1. 浏览器进程:主要用来控制浏览器部分,比如地址栏、书签栏、后退和前进的按钮,当然还有一些不可见的部分,比如网络请求和文件的处理,同时也负责其它进程的调度

  2. GPU 进程:跟其它进程关系不大,主要用来独立处理 GPU 任务,比如整个应用程序的绘制,网页的成像功能

  3. 渲染进程:负责网站的渲染,代码运行,web worker 的管理

  4. Network 进程、Storage 进程、Audio 进程等看名字就知道是用来干嘛的

  5. 百度标签页进程 &谷歌标签页进程:表明一个标签页就是一个进程

  6. 扩展程序进程:我们安装的一些插件,每个插件也占用一个进程


多进程优缺点

为什么每个标签或者每个插件都要一个进程呢?其实思考一下不难理解,如果我们打开的某一个网页无响应了,或者奔溃了,这个时候每个进程是隔离的,并不会影响我们其它的页面,这种设计其一是保证了标签页的稳定性。同样的如果某个页面死循环不流畅,其它页面也是感知不到的,内存泄漏也一样,当前页面由于内存泄漏关闭这个标签页之后,对应的内存资源就会被回收,变相解决了内存泄漏导致整个浏览器奔溃的问题

多进程还有一个好处就是安全性,因为操作系统有一种限制进程权限的方法,所以浏览器可以通过某些功能对某些进程采取沙箱处理,比如浏览器限制了渲染进程的任意文件的访问。沙箱处理隔离了渲染进程,这样即使在渲染进程里面执行恶意程序,恶意程序也无法突破沙箱获取系统的权限

当然并不是多进程都是好的,也有其不足之处,比如我打开了多个百度页面的选项卡,这个时候浏览器分配了多个进程,导致更多的资源占用,虽然这两个内存中的东西完全一致


当然谷歌浏览器针对这一点也做了对应的优化,它的内部限制了可以启动的进程数量,这个限制取决于你的设备的内存和 cpu 的功率,并不是固定死的,设备越好可启动的进程数量就越多。当达到它所限制的数量时,它会优化打开的标签页,比如相同站点的标签页合并为同一个进程

当然多个标签跟开启多个浏览器类似,谷歌浏览器也在不断优化,将浏览器中的各个部分作为一项服务,从多进程模型到多服务模型,可以轻松的进行进程拆分或者合并。也就是说当你的硬件性能足够,它可以将每个服务拆分到不同的进程,当你的硬件资源有限,它会将这些服务合并到一个进程

站点隔离

前面说我们每个标签页一个进程,但是这个标签页当中有可能通过 iframe 嵌入了另一个页面,这个时候如果公用一个进程的话可能就会带来风险。所以谷歌浏览器为跨站点的 iframe 页面也开启了一个单独的渲染进程。要考虑浏览器同源策略的影响,一个站点无法在未经允许的情况下访问其它站点的数据,进程隔离是分割站点的最有效的方式

站点隔离并不是我们想象的这么简单,它改变了 iframe 和页面的交互方式,即便是多个渲染进程,当你打开 devtools 的时候,它们看起来还是那么完美,并且当你用 ctrl+f 对页面进行搜索的时候,多个渲染进程之间的搜索工程师们也优化的你看不出丝毫破绽

了解了浏览器的架构,来讲一个面试官最喜欢问的面试题,当浏览器输入 url 之后发生了什么?

输入 url 之后发生了什么

我们使用浏览器的主要目的就是为了搜索或者访问某些网站,就让我们从浏览器的角度,来看看我们是如何进行搜索或者网站的访问的


从浏览器的架构中我们可以得知,我们输入 url 或者搜索的这一栏是由浏览器进程控制的,其中浏览器进程下面有一些线程,比如控制搜索栏交互和展示的 UI 线程,当你输入网址或者文字之后,UI 线程便开始了工作

输入还是搜索

当你在地址栏输入了内容之后,UI 线程要做的操作就是需要进行辨别,判断你输入的是 url 还是搜索的字段,如果是 url,则相应的转到对应的站点。如果是搜索的字段,则通过浏览器中设置的使用那种搜索引擎,进行对应的站点跳转


不论是搜索还是站点访问,最终都会走站点访问的逻辑,当你在地址栏输入【你好】之后,回车,它也会变成相应的站点 url


如何判断是否是 URL

要判断是否是 URL 就要知道什么是 URL(「U」niform 「R」esource 「L」ocator)翻译过来为统一资源定位符,俗称网址

它的标准格式为:

[协议类型]://[服务器地址]:[端口号]/[资源层级UNIX文件路径][文件名]?[查询]#[片段ID]

一个简单的 URL 组成如下所示

一个完整的 URL 如下所示


只要输入的内容满足这个规则,就认为是一个有效的 URL,直接跳转到对应的网站,否则就执行搜索逻辑(本质还是跳转到对应的 URL)

获取内容

拿到 URL 之后,是不是立刻就会发送请求?其实不是,浏览器还会进行额外的一些检查。

比如安全性检查,检查要访问的内容在本地是不是有缓存,缓存是否过期?一个较为完整的前置流程判断大致如下

这张图其实画的有点多,但是主要是想让大家了解在获取资源之前,其实是有一个缓存的判断的,否则就不会发送对应的请求


在控制台能够看到一些资源虽然返回了 200 的状态码,但是实际是来自缓存,并没有从服务器获取数据,抓包的话也是没有对应的请求的

强缓存和协商缓存

上面其实讲的是强缓存,强缓存是有对应的过期时间的,时间是响应标头 expires 控制,当然图中还有标注 cache-control 这个字段,这两个字短有啥区别呢?

expires 是 http1.0 的产物,cache-control 是 1.1 的产物,两个同时存在,cache-control 的优先级更高


图中还有一个字段,是 last-modified,这个主要用于协商缓存,如果强缓存生效,则直接走强缓存,不生效则走协商缓存,协商缓存会在请求头中添加 if-Modified-Since 的字段,这个字段的值就是第一次请求文件的时候返回头中的 last-modified,服务收到请求后,对比服务中文件修改的时间,如果没变化,状态码返回 304,浏览器直接从缓存中获取文件,如果有更新,则服务端会返回 200 状态,并返回新的文件,最后浏览器再将新的响应报文和对应的缓存标识缓存起来

协商缓存还有一个字段是 etag,图中也有,它是根据文件内容是否有修改来决定是否使用缓存数据

上述的缓存有些内容可能还未涉及,但是大家先了解一个大概,像请求报文之类的我们后续会详细说明,因为这里涉及到缓存,大家有个概念即可,我们看一下正常的请求流程是什么样的,不走缓存的这种

DNS 解析

如果缓存都未命中,我们就需要浏览器去发送请求,请求网址对应的资源,但是我们都知道,服务器的地址都是一段 ip 地址,但是我们明明输入的是一个 URL,URL 怎么能够知道我们访问的是哪一个服务器呢?

这就引出了 DNS 的概念,DNS 其实就是用于实现域名和 IP 相互映射的一个分布式数据库,它可以将域名翻译成计算机可识别的 IP 地址

借用网上的一张图,来看看 DNS 的查询流程是怎样的


  1. 看浏览器中是否有 URL 对应的 IP 缓存

  2. 当浏览器中没有的时候,查看系统的 hosts 文件是否有域名对应的 IP


  1. 查看路由器缓存(上述两种都没有的时候)这些都是客户端的缓存

  2. 当客户端没有对应缓存的时候,开始询问服务器

  3. 进入 ISP DNS 缓存,也就是运营商缓存,比如你用的移动或者联通的宽带,他们有自己的 DNS 缓存服务器

  4. 当以上均没有,则进入根域名服务器进行查询,全球就 13 台根域名服务器,1 个主的,12 个辅的。若无则将其管辖范围内顶级域名(如.com、.cn 等)服务器 IP 告诉本地 DNS 服务器

  5. 询问顶级域名服务器,若无记录则将其管辖范围内权威域名服务器的 IP 地址告诉本地 DNS 服务器

  6. 询问权威域名(主域名)服务器

  7. 本地域名服务器把返回的结果保存到缓存,以备下一次使用,同时将该结果反馈给客户端,客户端拿到了对应的 IP 地址,就能访问到对应的服务器,DNS 查询结束

DNS 是极其重要的一环,这个环节出了问题就无法进行后续的操作,它是客户端访问互联网的关键所在

建立 TCP 连接

通过 DNS 解析我们已经获取到了文件所在的服务器的 IP,有了这个 IP 之后,我们就需要发送请求获取对应的文件了,但是在获取文件的第一步,首先要做的就是建立 TCP 连接

一个页面的性能的影响因素首要的就是首屏渲染时间,而首屏渲染时间其中的一个影响因素就是网络加载速度,决定性的内容就是文件的返回时间

如果你对网络有充分的了解,那肯定知道网络的基础是网络协议,网站则是基于 http 协议,http 又是基于 TCP/IP 的。计算机底层是 101010 这种二进制数据,文件传输也是二进制数据,那这些数据是如何到我们的浏览器的?第一步就是要建立服务器与客户端之间的连接


上面我们已经获取到了服务端的 IP 地址,每个计算机都有一个独立的 IP 地址,客户端和服务端有了各自的地址之后就能够精准传输了,整个过程如下图所示

建立连接

建立连接的过程就是三次握手的过程,表示整个过程要发送三次包


  1. 客户端对服务端发起连接请求,携带 syn=1、seq=x 的报文

  2. 服务端接收到了客户端的请求,如果确认可以连接,则返回报文,携带 syn=1、ack=x+1、seq=y

  3. 客户端收到服务端发送的报文之后,检查是否符合要求,通知应用连接已经建立,向服务端发送确认信息 seq=z,ack=y+1,服务端校验正确则建立成功

连接建立成功,就可以传输数据了

数据传输完成,有一个断开的过程

连接断开

断开连接被称为四次挥手过程,表示整个断开过程要发送四次包,需要双向连接和双向关闭,不管是客户端还是服务端,任何一方都可以发起断开的过程


  1. 首先主动方要求断开连接,发送 fin=1 ack=z seq=x 的报文,请求断开连接

  2. 被动方接收到报文,发送 ack=x+1、seq=z 表明我知道了

  3. 发送完数据后,再发送一个 fin=1、ack=x、seq=y 的报文,告诉主动方数据发送完毕

  4. 主动方收到之后,发送 ack 表明确认收到,tcp 连接断开

获取资源

上一步已经建立好了连接,那就开始传输数据了。在该阶段,客户端需要对数据包进行确认操作,在接收到数据包之后,需要发送确认数据包给发送端。所以发送了一个数据包之后,在规定时间内没有接收到客户端端反馈的确认消息,则判断为数据包丢失,并触发发送端的重发机制。同样,一个大的文件在传输过程中会被拆分成很多小的数据包,这些数据包到达接收端后,接收端会按照 TCP 头中的序号为其排序,从而保证组成完整的数据。

请求头

通过 TCP 以及 UDP 共同作用,这个时候浏览器的网络线程是能够收到服务器的完整数据,在获取数据的时候,我们会添加一系列的请求头,比如我们必须指定请求方法到底是 GET 还是 POST,或者是其它,之前我们也提到了 If-Modified-Since,用于缓存,还有 Cookie、User-Agent、Referer 等头信息

用这些请求头数据去告诉服务器我们当前需要什么内容,以及告诉服务端客户端的一些信息


响应头

当服务端同意或者拒绝给客户端返回内容之后,客户端都会收到一些反馈,反馈的内容除了正常我们想要的数据以外,还有 response 头信息。可以看到对应的状态代码为 200,表示成功,说明获取到了服务端返回的对应信息。

这个域名对应的 IP+端口就是图中的远程地址,除了状态为 200 还有其它的一些状态,比如 301 告知服务器正在重定向,然后网络线程发起另一个 URL 请求,针对 http 状态码各个阶段的含义大家可以自行了解一下

  1. 1XX Informational(请求正在处理)

  2. 2XX Success(请求成功)

  3. 3XX Redirection(重定向) 需要进行附加操作以完成请求

  4. 4XX Client Error(客户端错误)

  5. 5XX Server Error(服务器错误)

上面的图中可以看到响应头里面包含了一些信息或者执行了一些操作,比如 Set-Cookie 响应头可以往浏览器里面设置一些 cookie,Conetnt-Type、Cache-Control 等相关

网络线程在查看了头部的这些字节之后,因为传输过程有可能会出现异常,比如丢失或者错误,所以在这里会完成 MIME 类型嗅探(也就是检查一个字节流的内容,试图推断其中文件的数据格式)


上面我们在访问百度的时候,Conetnt-Type 为 html 类型的文件,浏览器检测到如果文件类型是 html 的话,之后就会把数据交给渲染进程。当然这里不仅仅只有 html 文件类型,如果是其它文件类型,比如 zip 或者其它的内容,则浏览器会交给下载管理器进行对应资源的下载

这个时候通常也会进行浏览器的安全检查,两方面检查

  1. 如果这个站点和已知的恶意站点匹配,则网络线程发出警告,表明这是一个恶意站点


  1. 还有一个检查的点大家都比较熟悉,那就是跨域问题的检测,跨域本质是浏览器的安全检查机制,如果发现请求的 URL 的协议域名端口任意一个和当前站点不同即为跨域,这个检查也会在这个阶段,确保敏感的跨站点的数据不会进入渲染进程

所以我们要明确的一点是,跨域是浏览器的安全策略,是浏览器拦截的,如果你用抓包工具的话,会发现数据其实已经给到我们了,当然 post 请求还会存在一个预检的过程,防止抓到数据,在发正式请求之前,预检服务端是否做了跨域的处理

渲染

当前已经准备好了对应的数据,也就是 html 文件。并且完成了前置的所有信息检查,那么网络线程就会告诉 UI 线程数据已经准备就绪,UI 线程要做的就是找一个渲染进程用于 html 的渲染

但是这个过程是有优化的空间的,因为网络线程请求数据的过程是需要时间的,所以在网络线程发送 URL 的请求的时候,它已经知道当前是要访问哪个站点,UI 线程将会并行查找并启动渲染进程,这个时候请求到数据的时候,渲染进程已经是待命的状态,可用于直接渲染

这个时候需要浏览器进程跟渲染进程通过 IPC 进行通信,通信过程还需要传递数据流,方便渲染进程可以持续接收 html 数据,一旦渲染进程渲染完毕,便会通知浏览器进程当前完毕,导航阶段就完成了,就开始了加载文档的过程

这个时候,地址栏更新。安全指示器和站点设置的 UI 反应站点的信息,选项卡的历史记录会被更新,前进后退等历史记录逐步被更新,历史记录同样也会在磁盘上存储一份,方便进行整个历史浏览的检索


我们知道,当页面进行加载的时候,浏览器 UI 上 tab 标签页上会有一个加载中的 loading 标志,一旦渲染进程完成渲染,渲染进程会将回调通过 IPC 发送到浏览器进程(onload 事件完成的时候,包含所有子页面(frame)),浏览器 UI 上 loading 标志消失,显示完成状态,但是这个结束并不代表页面渲染就完成了,有可能还有 JavaScript 在加载额外的资源或者新的视图




这个时候渲染进程便开始渲染,具体是如何渲染的我们之后详细讲述,我们再看一下在这基础如何访问另一个页面

访问不同站点

在当前标签页,我们进行另一个页面访问的时候,浏览器进程会重复上面的过程。但是开始的时候,浏览器会确认当前的站点是否关心 beforeunload 这个事件,如果对这个事件做了监听,当访问另一个网站或者刷新的时候,就会弹出一下选项进行确认




window.addEventListener('beforeunload', (event) => {  // 显示确认对话框  event.preventDefault();  // 为了兼容处理,Chrome需要设置returnValue  event.returnValue = '';});
复制代码

当然,这只是众多生命周期中的一个节点,还有比如 unload,pagehide,pageshow 等等,感兴趣的可以参照https://developers.google.com/web/updates/2018/07/page-lifecycle-api

service worker

这里我们再插入一个知识点,service worker,它的作用是什么呢?其实就是服务器和浏览器之间的一个中间人。目的是为了拦截网站的所有请求,可以进行相应的判断,如果一些接口可以直接使用缓存就直接返回缓存

service worker 独立于当前网页的线程,所以执行大量的操作也不会阻塞主线程

渲染进程如何工作

上面的过程把 html 文件已经交给了渲染进程,渲染进程负责页签的显示,在一个渲染进程中,主线程负责解析,编译代码,运行等工作,它的核心就是将 HTML、CSS 和 JavaScript 转换成用户可以与之交互的网页

当然渲染进程是一个多线程架构,它主要有以下线程:GUI 线程、JavaScript 引擎线程、定时器触发线程、事件触发线程、http 请求线程、合成线程和 IO 线程

GUI 渲染线程

拿到数据之后,GUI 渲染线程就开始解析 HTML 并将其转换成 DOM(文档对象模型),DOM 是浏览器对页面的内部表示,javascript 获取和操作的页面元素本质是浏览器提供的 DOM 数据,同时当页面发生重绘和回流的时候,该线程也会执行

在解析过程中,即便是你的 html 语法有一些异常,比如没有关闭标签,匹配错误等,浏览器也不会抛出异常,比如如下代码,在浏览器上会自动解析成功

<body>  <div>  </p></body>
复制代码



这里还需要注意一点的是,GUI 渲染线程和 JavaScript 引擎线程是互斥的,当 JavaScript 引擎线程执行的时候,GUI 线程是被挂起的,相当于是冻结状态,GUI 的更新会被保存在一个队列中等 JavaScript 引擎空闲的时候立刻执行。之所以这样是因为 JS 代码可能会改变 DOM 结构,所以 JavaScript 引擎执行时间过长是会阻塞页面的渲染的,了解这一点也就知道为什么 fiber 架构为什么能够让大型应用看起来不卡顿

在解析 html 的过程中,其实还有一些其它的资源,比如 img 或者 link,这个时候就会给浏览器进程的网络线程发送信息,GUI 线程会根据这些额外的资源是否会阻塞转换过程而决定是不是需要资源加载完毕。比如碰到 script 标签有可能就会阻塞,但是也有例外,script 标签添加了 async 或者 defer 属性

JavaScript 引擎线程

负责解析 JavaScript 脚本,运行代码

事件触发线程

比如我们的点击事件,滚动事件,异步请求,或者执行 setTimeout 等这些事件时,会将对应的任务添加到事件触发线程,当这个事件被触发的时候,则把触发的事件回调添加到待处理队列的队尾。由于 javascript 是单线程的,所以处理这些事件都必须排队

定时器线程

setInterval 与 setTimeout 所在线程,计数通过上面的内容,可以得到不可能通过 javascript 线程计数,否则会阻塞,因此会有一个单独的线程进行计数的处理,等待时间达到后,将回调函数添加到事件队列

HTTP 请求线程

在 XMLHttpRequest 连接后是通过浏览器新开一个线程请求,监测到获取了对应的内容后,将回调函数添加到事件队列,再由 javascript 引擎执行

合成线程后面会讲,IO 线程主要用于和其它的线程通信

布局

当前 DOM 已经有了,但是精美的页面光有 DOM 是不够的,只有 DOM 是不会出现我们的五彩斑斓的页面,需要 CSS 让页面变得更美观,GUI 线程会解析 CSS 并决定每个 DOM 元素的样式


如果你没有设置对应的样式,浏览器也有自己的内置的一些标签样式,比如 h1-h6

有了样式,渲染进程已经知道了每个结点呈现的效果,但是节点的位置信息怎么来,这个时候需要布局树,渲染进程会遍历 DOM 结构(包含样式),布局树只包含在页面中显示的元素,当一个元素被设置为 display: none 的时候布局树中是没有这个元素的。同理如果 div::before { content: 'Hi!' },则布局树中是存在这个 Hi 的,DOM 树 javascript 能够获取,但是布局树获取不到

布局树的描述非常具有挑战性,因为你需要对整个页面进行精确的描绘。布局中存在浮动、定位、固定、文字换行,自动伸缩,各种元素的结合,可以想象这个任务多么繁重

绘制

我们有了布局树的信息之后是不是就能绘制了,其实并不是,虽然你知道了每个元素的位置,但是它们绘制的顺序是怎样的其实还是不清楚,到底哪个元素先,哪个元素后,了解 PS 的同学,肯定知道图层的概念,哪个元素应该在哪个元素的顶部?CSS 有控制元素层级的一个属性,叫做 z-index,用过的同学应该都了解


这个阶段会通过布局树形成绘制记录,绘制记录本质就是绘制的一系列步骤,比如我要先干什么,在干什么(先绘制背景,再绘制元素内容,再绘制形状等等)

渲染

渲染的过程开销是很大的,任何一个小的变化都会引起一系列的变化,当布局树发生变化的时候,绘制需要重新构建页面变化,页面有动画的效果的时候,每一帧都需要更新动画内容,如果无法保证帧动画,给用户感官上就会出现卡顿




javascript 也会阻塞页面的渲染,导致卡顿的发生,可以将 Javascript 操作优化成小块,然后使用 requestAnimationFrame()

  • 重排:表示布局树发生变化的时候,整个布局树要重新构建,形成绘制记录,重新回炉修炼

  • 重绘:不影响元素位置信息的,比如元素的颜色发生变化,但是元素位置未发生改变,只需要重绘

合成

目前我们已经有了所有的信息,文档结构-元素样式-元素几何-布局树-绘制记录,最终将绘制记录转换到屏幕上的像素称之为光栅化

之前的方式是可视区域进行光栅化,滚动的时候再次进行光栅化,如下所示

AiIny83Lk4rTzsM8bxSn.gif

但是现在浏览器有着更好的处理方式,这个方式被叫做合成

合成会将一个页面拆成很多层,每个层在不同的的合成线程中进行光栅化,然后组合成一个新的页面。滚动过程中如果这个层已经光栅化,则使用已经光栅化的层进行合成

Aggd8YLFPckZrBjEj74H.gif

那这个时候问题就来了,一个层中要包含哪些元素呢?主线程需要遍历布局树,做为开发者,想要创建一个新的层,可以使用 css 属性 will-change 让浏览器创建层

光栅化各个层之后,将其存储在 GPU 的缓存中,合成线程也能够决定相应的优先级,保证用户看到的部分最先被光栅化,每当有交互发生变化,合成线程就会创建更多的合成帧然后通过 GPU 将新的部分渲染出来

浏览器的事件体系

事件是什么?比如按钮的点击,input 输入框的内容输入,鼠标滚轮和拖拽,都是事件

交互的时候浏览器进程最先接收到事件,浏览器关心的只有当前事件发生在哪个页签,然后将事件位置信息和事件类型发送到当前页签的渲染进程,渲染进程会找到事件发生的元素和对应的事件


但是前面也说到,页面是被光栅化的,在合成线程处理页面的时候,合成线程会标记有事件监听的区域,有这些信息,合成线程就会将触发的事件发送给主线程处理

总结

浏览器的多进程架构,根据不同的功能划分了不同的进程,进程内不同的使命划分了不同的线程,当用户开始浏览网页时候,浏览器进程进行处理输入、开始导航请求数据、请求响应数据,查找新建渲染进程,提交导航,之后渲染又进行了解析 HTML 构建 DOM、构建过程加载子资源、下载并执行 JS 代码、样式计算、布局、绘制、合成,一步一步的构建出一个可交互的 WEB 页面,浏览器进程又接受页面的交互事件信息,并将其交给渲染进程,渲染进程内主进程进行命中测试,查找目标元素并执行绑定的事件,完成页面的交互。

刚踏上开发之路时,我几乎只关注怎样去写代码、怎样提升自己的生产效率。诚然,这些事情很重要,但与此同时我们也应当思考浏览器会怎么去处理我们书写的代码。现代浏览器一直致力探索如何提供更好的用户体验。书写对浏览器友好的代码,反过来也能提供友好的用户体验。希望能够通过这节课让大家了解浏览器的运行机制和原理,构建出对浏览器更为友好的代码。同时也能够不断优化我们的业务,让用户体验更上一层楼,这就是本节课全部内容

最后,感谢大家阅读,码字不易,一键三连

发布于: 15 小时前阅读数: 4
用户头像

FE情报局

关注

还未添加个人签名 2018-11-08 加入

还未添加个人简介

评论

发布
暂无评论
【万字爆肝】带你了解浏览器原理_JavaScript_FE情报局_InfoQ写作社区