浏览器是如何运作的?

207 阅读11分钟

重要性:写出更好的代码,提供更好的用户体验

浏览器的发展史

  • 1991年, Berners Lee 建立了第一代网络浏览器 WorldWideWeb 。功能十分简单,只支持显示文本图片。
  • 1993年Mosaic 问世,可以同时显示文本和图片,一经推出就受到全球用户的欢迎。
  • 1994年,网景浏览器发布,它由曾经参与开发 Mosaic 的人共同创建。虽然只能显示简单的静态 html 网页,没有 js、css,但依然大受欢迎,获得世界范围内的成功,并占领了绝大多数市场份额。
  • 同年,出现了 Opera
  • 1995年,微软发布了 IE 1.0IE 2.0,自此,第一次浏览器大战正式打响。
  • 1996年,微软紧接着发布的 IE 3.0 和 windows 操作系统集成在了一起,而此时网景的市场份额已经达到了86%
  • IE 发行后的四年内,在 windows 操作系统的帮助下,逐渐取代了网景浏览器的领导地位,达到了市场份额的 75%
  • 1998年,网景成立了 Mozilla 基金会,在该基金会的推动下,网景公司开发了著名的开源项目 --- 火狐浏览器 Firefox 来迎击 IE。
  • 1999年IE 浏览器已经占据了浏览器市场份额的99%
  • 2003年,苹果发布了 Safari浏览器,而且该浏览器被包含在所有的苹果操作系统中。
  • 2004年,网景发布 Firefox 1.0 ,拉开了第二次浏览器大战的序幕。
  • 2005年,苹果开源了 Safari 浏览器的内核 webkit
  • 2008年,谷歌以苹果开源项目 webkit 作为内核,创建了一个新的项目 Chromium ,在该项目的基础上,谷歌发布了自己的浏览器产品 Chrome ,Chrome 发展也十分迅速,现在已经成为了全球最受欢迎的浏览器。
  • 2015年,由于 IE 的性能和体验问题,IE 逐渐掉队。微软放弃 IE 推出了基于 webkit 内核的 Edge 浏览器 作为 IE 的替代品 ,但为时已晚。

根据 StatCounter 的统计,截止 2020 年 5 月份,web 浏览器的市场份额如下:

ChromeSafariFirefoxSamsung InternetEdge LegacyUc Browser
63.91%18.2%4.39%3.28%2.13%2%

浏览器结构

用户界面

  • 用于展示除标签页窗口之外的其他用户界面内容

浏览器引擎

  • 用于在用户界面和渲染引擎之间传递数据

渲染引擎(浏览器内核)

  • 负责渲染用户请求的页面内容,是一个浏览器的核心与灵魂。 渲染引擎下面还有很多小的功能模块:
  • 网络模块:负责网络请求
  • JS解析器:用于解析和执行js
  • 数据存储持久层:帮助浏览器存储各种数据,比如cookie。

不同浏览器使用的内核不大相同:

IEFirefoxSafariChromeOperaEdge
TridentGeckoWebkit(开源)Blink(基于Webkit 开源)BlinkBlink

Webkit 的开源对浏览器的发展作出了非常大的贡献。

浏览器的运作原理(以Chrome为例)

进程和线程

  • 浏览器是运行在操作系统上的一个应用程序。当我们启动某个程序时,就会创建一个进程来执行任务代码,同时会为该进程分配内存空间,该应用程序的状态都保存在该内存空间里。

  • 当应用关闭时,该内存空间就会被回收。进程可以启动更多的进程来执行任务,由于每个进程分配的内存空间是独立的,如果两个进程间需要传递某些数据,则需要通过进程间通信管道 IPC 来传递,很多应用程序都是多进程的结构,这样是为了避免某一个进程卡死。

  • 由于进程间相互独立,这样不会影响到整个应用程序。进程可以将任务分成更多细小的任务,然后通过创建多个线程并行执行不同的任务。同一进程下的线程之间是可以直接通信共享数据的。

进程:操作系统进行资源分配和调度的基本单元,可以申请和拥有计算机资源,进程是程序的基本执行实体。

线程:操作系统能够进行运算调度的最小单位,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

早期的浏览器是单线程结构,一个进程中大概有页面线程负责页面渲染和展示等,JS线程执行JS代码,还有其他各种线程。单进程的结构引发了很多问题

  1. 不稳定,其中一个线程的卡死可能会导致整个进程出问题;
  2. 不安全,浏览器之间是可以共享数据的,Js线程可以随意访问浏览器进程的所有数据;
  3. 不流畅,一个进程需要负责太多事情,会导致运行效率的问题

现在采用多进程浏览器结构,根据进程功能不同来拆卸浏览器,可以分为如下结构:

  1. 浏览器进程:负责控制 Chrome 浏览器除标签页外的用户界面,包括地址栏、书签、后退和前进按钮,以及负责与浏览器的其他进程协调工作。
  2. 网络进程:负责发起接受网络请求。
  3. GPU进程:负责整个浏览器界面的渲染。
  4. 插件进程:负责控制网站使用的所有插件,例如:flash。这里的插件并不是指的 Chrome 市场里安装的扩展。
  5. 渲染器进程:用来控制显示 tab 标签内的所有内容,浏览器会在默认情况下为每个标签页都创建一个进程(Chrome有四种进程模型可选,默认为每个标签页都创建一个进程)。
  6. 缓存进程

当你在浏览器的地址栏输入地址时会发生什么?

浏览器进程的UI线程会捕捉你的输入内容。如果访问的是网址,则 UI 线程会启动一个网络线程来请求 DNS 进行域名解析,接着开始连接服务器获取数据;如果你的输入不是网址而是一串关键词,浏览器就知道你是要搜索,于是就会使用默认配置的搜索引擎来查询 。

网络线程获取到数据后会发生什么?

  • 当网络线程获取到数据后,会通过 SafeBrowsing 来检查站点是否是恶意站点,如果是,则会提示个警告页面。SafeBrowsing 是谷歌内部的一套站点安全系统,通过检测该站点的数据来判断是否安全。比如通过查看该站点的 IP 是否在谷歌的黑名单之内。

  • 当返回数据准备完毕,并且安全校验通过时,网络线程会通知 UI 线程我就要准备好了,UI 线程会创建一个渲染器进程(Renderer Thread)来渲染页面,浏览器进程通过 IPC 管道将数据传递给渲染器进程,正式进入渲染流程。

  • 渲染器进程接收到的数据也就是 html ,渲染器进程的核心任务就是把 html、css、js、image 等资源渲染成用户可以交互的 web 页面,渲染器进程的主线程将 html 进行解析,构造 DOM 数据结构。DOM 也就是文档对象模型,是浏览器对页面在其内部的表示形式,是 web 开发程序员可以通过 JS 与之交互的数据结构和 API 。

  • html 首先通过 tokeniser 标记化 ,通过词法分析将输入的 html 内容解析成多个标记,根据识别后的标记进行 DOM 树构造,在 DOM 数构造过程中会创建 document 对象,然后以 document 为根节点的 DOM 树不断进行修改,向其中添加各种元素。

  • html 代码中引入的额外资源需要通过网络下载或者从缓存中直接加载,这些资源不会阻塞 html 的解析,因为他们不会影响 DOM 的生成。但当 HTML 解析过程中遇到 script 标签,就会停止 html 解析流程,转而去加载解析并且执行JS。这是因为浏览器并不知道 JS 执行是否会改变当前页面的 html 结构,所以 script 标签要放在合适的位置,或者使用 async 或 defer 属性来异步加载执行。

  • 之后,主线程需要 解析 css ,并确定每个 DOM 节点的计算样式,即使你没有提供自定义的 css 样式,浏览器也会有自己默认的样式。

  • 接下来,需要知道每个节点的坐标以及该节点需要占用多大的区域,这个阶段被称为 Layout 布局 ,主线程通过遍历 dom 和计算好的样式来生成 Layout Tree 。注意:DOM Tree 和 Layout Tree 并不是一一对应的,设置了 display:none 的节点不会出现在 Layout Tree 上,而在 before 伪类中添加了 content 值的元素,content 里的内容会出现在 Layout Tree 上,不会出现在 DOM 树里。Layout Tree 是和最后展示在屏幕上的节点是对应的。

  • 节点绘制(paint)的层级关系受 z-index 属性的影响,为了保证在屏幕上展示正确的层级,主线程遍历 Layout Tree 创建一个绘制记录表(Paint Record),该表记录了绘制的顺序,这个阶段被称为绘制(paint)。

  • 知道了文档的绘制顺序,到了把这些信息转化成像素点,显示在屏幕上的时候了,这种行为被称为栅格化(Rastering)。Chrome 现在使用的栅格化流程是合成( Composting),合成是一种将页面的各个部分分成多个图层,分别对其进行栅格化,并在合成器线程(Compositor Thread)的技术中单独进行合成页面。主线程遍历 Layout Tree 生成 Layer(图层)Tree,当 Layer Tree 生成完毕和绘制顺序确定后,主线程将这些信息传递给合成器线程,合成器线程将每个图层栅格化。由于一层可能像页面的整个长度一样大,因此合成器线程将它们切分为许多图块(tiles),然后将每个图块发给栅格化线程(Raster Thread),栅格化线程栅格化每个图块,并将它们存储在 GPU 内存中。当图片栅格化完成后,合成器线程将收集称为“draw quads“的图块信息,这些信息里记录了图块在内存中的位置和在页面的哪个位置绘制图块信息。根据这些信息合成器线程生成了一个合成器帧(Compositor Frame),然后合成器 Frame 通过 IPC 传送给浏览器进程,接着浏览器进程将合成器帧传送到 GPU ,然后 GPU 渲染展示到屏幕

  • 重排:当我们改变一个元素的尺寸位置属性时,会重新进行样式计算(Computed Style)、布局、绘制及后面的所有流程。重绘:当我们改变某个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制。

  • 重排和重绘都会占用主线程,Js也是在主线程运行,就会出现抢占执行时间的问题,从而导致页面卡顿。优化解决办法:

    1. 通过 requestAnimationFrame() 这个 API 来帮助我们解决这个问题,这个方法会在每一帧被调用,通过 API 的回调,然后我们可以把 JS 的运行任务分成一些更小的任务块(分到每一帧)在每一帧时间用完前暂停 JS 执行,归还主线程。React 的最新渲染引擎 React Fiber 就是用到了这个 API 来做了很多优化
    2. 栅格化的整个流程不占用主线程,使用 css 中的 transform 动画属性,通过该属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格化线程中,所以不会受到主线程中 JS 执行的影响,并且可以节省运行时间