快捷搜索: 长连接 前端 源码 pan

浏览器工作原理:从 URL 输入到页面展现到底发生了什么?

浏览器工作原理:从 URL 输入到页面展现到底发生了什么?

这个问题是一个大坑。

可以回答的特别简单,40个字就可以概括。

但是如果深究,这背后的原理架构涉及计算机网络,操作系统,编译原理等等好几门大课。

乞丐版:

  1. DNS 解析
  2. TCP连接
  3. 发送HTTP请求
  4. 服务器处理请求并返回HTTP报文
  5. 浏览器解析并渲染页面
  6. 连接结束

对,没错,就是这么简单。

豪华版:

  1. 用户输入信息并回车
  2. 根据用户输入的信息判断是内容还是网址。如果是内容,进行搜索。如果输入的内容符合URL规则,根据 URL 协议,构成完整的 URL。
  3. 浏览器进程通过进程间通信(IPC)把 URL 发送给网络进程
  4. 网络进程接收到 URL 请求后检查本地缓存是否缓存了该请求资源,如果有,则将该资源返回给浏览器进程
  5. 如果没有,网络进程向 web 服务器发起网络请求,请求流程如下: 进行 DNS 解析,获取服务器 ip 地址,端口 利用 ip 地址和服务器建立 TCP 连接 构建请求头并发送 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容
  6. 网络进程解析响应流程 检查状态码,如果是 301/302 ,则需要重定向,从 Location 自动中读取地址,重新进行网络资源判断,如果是200,则继续处理请求。 200响应处理:检查响应类型 Content-Type,如果是字节流类型,则将该请求提交给下载管理器,导航流程结束,不再进行后续的渲染,如果是 HTML 则通知浏览器进程准备渲染进程准备进行渲染。
  7. 准备渲染进程 浏览器进程检查当前 URL 和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,反之则开启新的渲染进程。
  8. 传输数据、更新状态 渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的管道 渲染进程接收完数据后,向浏览器发送“确认提交” 浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏 URL、前进后退的历史状态、更新web页面。
  9. 渲染进程开启 渲染进程对文档进行页面解析和子资源加载 HTML 通过 HTML 解析器转成 DOM Tree CSS 按照 CSS 规则和 CSS 解释器转成 CSSOM Tree 两个 tree 结合,形成 render tree(不包含HTML 的具体元素和元素的具体位置) 通过 Layout 从根节点递归调用,计算每一个元素的大小、位置等,给出每个节点所应该在屏幕上出现的精确坐标。(dom tree 与 layout tree 并不是一一对应的,比如设置了 display:none的属性就不会在 layout 上显示,而样式表中如果设置了 ::before 属性,则会在 layout 上显示。) 主线程遍历 layout tree 创建一个绘制记录表(Paint Record)记录元素绘制的顺序。 将信息转换为像素点,显示在屏幕上。这样的行为被称为栅格化(Rastering) 遍历渲染树,使用UI后端层来绘制每个节点。

你以为这就完了,不不不,开头说过。这个问题的背后涉及了太多。

笔者资历尚浅,其背后原理不是在下可以一一说明的。各位看此文章的同时,建议还是多翻翻专业书。

很多字版:

  1. 某个未知的时候,服务器启动开始监听服务 某个未知的时刻,一台机房里普普通通的服务器,加上电,启动了操作系统,随着操作系统的就绪,服务器启动了 http 服务进程,这个 http 服务进程开始定位到服务器上的网站根目录,然后启动了一些附属的模块,之后,向操作系统申请了一个 tcp 连接,然后绑定在了 80 端口,开始了默默的监听,监听着可能来自位于地球任何一个地方的请求,随时准备做出响应。 这个时候,机房里面应该还有一个数据库服务器,或许,还有一台缓存服务器,如果对于流量巨大的网站,那么动态脚本的解释器可能还有单独的物理机器来跑,如果是中小的站点,那么上述的服务,都可能在一台物理机上,他们做好了准备,静候差遣。
  2. 键盘输入信息并回车,通过 I/O 系统的处理,传至CPU,CPU进行内部处理后,从CPU传到操作系统内核,再由操作系统 GUI 传到浏览器,由浏览器到浏览器内核。 这过程中,浏览器可能会做一些预处理,甚至已经在智能匹配所有可能的URL了,会从历史记录,书签等地方,找到已经输入的字符串可能对应的URL,来预估所输入字符对应的网站,然后给出智能提示,比如输入了ba,根据之前的历史发现 90% 的概率会访问www.baidu.com,因此就会在输入回车前就马上开始建立 TCP 链接了。对于 Chrome这种变态的浏览器,甚至会直接从缓存中把网页渲染出来,就是说,你还没有按下回车键,页面就已经出来了,再比如Chrome会在浏览器启动时预先查询10个你有可能访问的域名等等。
  3. 浏览器进程通过进程间通信 IPC 把 URL 发送给网络进程。
  4. 网络进程接收到 URL,根据请求头中的 expries 和 cache-control 来判断是否命中强缓存。如果命中强缓存,则从缓存中读取资源。如果强缓存失效或者设置的字段被绕开,则发起请求到服务器,服务器根据请求头中的 If-Modified-Since(资源最后的修改时间)和 If-None-Match(文件的唯一标识符)判断缓存是否有效。若命中缓存,则返回 304,否则返回200和 资源,同时更新Last-Modified(资源最后的修改时间)和 Etag(文件的唯一标识符)。
  5. 网络进程则向 web 服务器发起请求。
  6. URL 解析 完整的URL由几个部分构成:协议,网络地址,资源路径,文件名,动态参数。 协议:是从该计算机获取资源的方式,一般有Http、Https、Ftp等协议,不同协议有不同的通讯内容格式,协议主要作用是告诉浏览器如何处理将要打开的文件。 网络地址:指示该连接网络上哪一台服务器,可以是域名或者IP地址,域名或 IP地址 后面有时还跟一个冒号和一个端口号 端口号:如果地址不包含端口号,根据协议的类型会确定一个默认端口号。端口号之于计算机就像窗口号之于银行,一家银行有多个窗口,每个窗口都有个号码,不同窗口可以负责不同的服务。端口只是一个逻辑概念,和计算机硬件没有关系。一般如果你的端口号就是默认的,那么 url 是不需要输入端口号的,但如果你更改了默认端口号,你就必须要在 url 后输入新端口号才能正常访问。 资源路径:指示从服务器上获取哪一项资源的等级结构路径,以斜线/分隔 文件名:一般是需要真正访问的文件,有时候,URL以斜杠“/”结尾,而隐藏了文件名,在这种情况下,URL引用路径中最后一个目录中的默认文件(通常对应于主页),这个文件常被称为 index.html 动态参数:有时候路径后面会有以问号?开始的参数,这一般都是用来传送对服务器上的数据库进行动态询问时所需要的参数,有时候没有,很多为了seo优化,都已处理成伪静态了。要注意区分url和路由的区别。 浏览器对 URL 进行检查时首先判断协议,如果是 http/https 就按照 Web 来处理,另外还会对 URL 进行安全检查,然后直接调用浏览器内核中的对应方法,
  7. 利用 ip 地址和服务器建立 TCP 连接 当应用层的 HTTP 请求准备好后,浏览器会在传输层发起一条到达服务器的 TCP 连接,位于传输层的TCP协议为传输报文提供可靠的字节流服务。它为了方便传输,将大块的数据分割成以报文段为单位的数据包进行管理,并为它们编号,方便服务器接收时能准确地还原报文信息。TCP协议通过三次握手等方法保证传输的安全可靠。 三次握手的过程是,发送端先发送一个带有SYN(synchronize)标志的数据包给接收端,在一定的延迟时间内等待接收的回复。接收端收到数据包后,传回一个带有SYN/ACK标志的数据包以示传达确认信息。接收方收到后再发送一个带有ACK标志的数据包给接收端以示握手成功。在这个过程中,如果发送端在规定延迟时间内没有收到回复则默认接收方没有收到请求,而再次发送,直到收到回复为止。
  8. 发送请求信息
  9. 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容 服务接收到客户端发送的HTTP请求后,服务器上的的 http 监听进程会得到这个请求,然后一般情况下会启动一个新的子进程去处理这个请求,同时父进程继续监听。http 服务器首先会查看重写规则,然后如果请求的文件是真实存在,例如一些图片,或 html、css、js 等静态文件,则会直接把这个文件返回,如果是一个动态的请求,那么会根据 url 重写模块的规则,把这个请求重写到一个 rest 风格的 url 上,然后根据动态语言的脚本,来决定调用什么类型的动态文件脚本解释器来处理这个请求。
  10. 网络进程解析响应流程 响应到达浏览器之后,浏览器首先会根据返回的响应报文里的一个重要信息——状态码,来做个判断。 如果是 300 开头的就要去相应头里面找 location 域,根据这个 location 的指引,进行跳转,这里跳转需要开启一个跳转计数器,是为了避免两个或者多个页面之间形成的循环的跳转,当跳转次数过多之后,浏览器会报错,同时停止。比如:301表示永久重定向,即请求的资源已经永久转移到新的位置。在返回301状态码的同时,响应报文也会附带重定向的url,客户端接收到后将http请求的url做相应的改变再重新发送。 如果是 400 或者 500 开头的状态码,浏览器也会给出一个错误页面。比如:404 not found 就表示客户端请求的资源找不到。 200响应处理:检查响应类型 Content-Type,如果是字节流类型,则将该请求提交给下载管理器,导航流程结束,不再进行后续的渲染,如果是 HTML 则通知浏览器进程准备渲染进程准备进行渲染。
  11. 准备渲染进程 浏览器进程检查当前 URL 和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,反之则开启新的渲染进程。
  12. 传输数据、更新状态 渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的管道 渲染进程接收完数据后,向浏览器发送“确认提交” 浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏 URL、前进后退的历史状态、更新web页面。
  13. 渲染页面 DOM 的创建 主线程开始解析 HTML 文本字符串,并且将其转化成 DOM。DOM 是一种浏览器内部用于表达页面结构的数据,同时也为 Web 开发者提供了操作页面元素的接口,让 web 开发者可以在 Javascript 代码中获取和操作页面中的元素。将 HTML 文本转化成 DOM 的转化过程中浏览器从来不会抛出异常,因为 HTML 标准中定义了要静默的处理这些错误。 在转化的过程中还会出现额外资源的加载。一个网站通常还会使用类似图片,样式文件和 JavaScript 代码等额外的资源。这些资源也需要从网络或缓存中获取。主线程在转化 HTML 的过程中理应挨个加载它们,但是为了提高效率,预加载扫描与转换过程会同时运行着。当预加载扫描在分析器分析 HTML 过程中发现了类似 img 或 link 这样的标签时,就会发送请求,而主线程会根据这些额外资源是否会阻塞转化过程而决定是否等待资源加载完毕。 转化过程也不是一帆风顺的,JavaScript 会阻塞转化过程。当 HTML 分析器发现<script>标签时,会暂停接下来的 HTML 转化工作,然后加载、解析并且运行 Javascript 代码。因为在 Javascript 代码中可能会使用类似document.write这样的 API 去改变 DOM 的结构。这就是为什么 HTML 分析器必须等待 Javascript 代码运行结束才能继续分析的原因。 如果 Javascript 代码并不需要改变 DOM,可以为<script>标签添加async或defer属性,这样浏览器就会异步的加载这些资源并且不会阻塞 HTML 转化过程。如果 script 标签是由 JavaScript 代码创建的,标签的 async 属性会默认为 true。同时我们也可以使用一些预加载技术来通知浏览器这些资源需要越快下载越好。 样式计算 对于展示一个页面,光有 DOM 是不够的,因为我们还需要样式来让页面变得更美观。主线程会解析样式(CSS)并决定每个 DOM 元素的样式。这些样式取决于 CSS 选择器的范围。当然即使我们没有给 DOM 指定任何的样式,<h1>标签也会比<h2>标签显示的大。这是因为浏览器为不同的标签内置了不同的样式。可以通过Chromium源代码得到这些默认样式。 布局(layout) 完成了样式计算工作后,渲染进程已经知道了 DOM 的结构和每个节点的样式,但是依然不足以渲染一个页面。 布局是为元素指定几何信息的过程。主线程遍历 DOM 结构中的元素及其样式,同时创建出带有坐标和元素尺寸信息的布局树(Layout tree)。布局树的结构与 DOM 树的结构十分相似,但只包含将会在页面中显示的元素。当一个元素的样式被设置成 display: none 时,元素就不会出现在布局树中,但那些样式被设置成 visiblility:hidden 的元素会出现在布局树中。相似的,当我们使用一个包含内容的伪元素(例如p::before { content: Hi! })时,元素会出现在布局树中,即使这个元素不存在于 DOM 树中,这也是为什么我们使用 DOM 提供的 API 无法获取伪元素的原因。 描述页面布局信息是一项具有挑战性的工作,即使在只有块元素的页面中也必须要考虑字体的大小和在哪里换行,因为在计算下一个元素的位置时需要知道上一个元素的尺寸和形状。 CSS 可以让元素浮动、可以让元素在父元素中溢出,可以改变文字的方向。可以想象,在布局这个阶段是多么繁重的工作。 绘制(Paint) 有了 DOM、样式和布局还是无法完成渲染工作。试想,当我们试图复制一张图画。我们知道图画中元素的尺寸、形状和位置,我们还需要知道绘制这些元素的顺序。 例如,当一个元素 z-index 属性被设置后,绘制的顺序会导致渲染成错误的结果。在这个阶段,主线程遍历布局树并创建绘制记录,绘制记录是一系列由绘制步骤组成的流程。 渲染过程是很昂贵的 在渲染过程中,任何一个步骤中产生的数据变化都会引起后续一系列的的变化。例如,当布局树改变时,绘制需要重构页面中变化的部分。 当一些元素有动画发生时,浏览器需要在每一帧中绘制这些元素。当无法保证每一帧绘制的连续性时,用户就会感觉到卡顿。 正常情况下渲染操作可以与屏幕刷新保持同步,但由于这些操作运行在主线程中,也就意味这些操作可能被正在运行的 Javascript 代码所阻塞。 为了不影响渲染操作,我们可以将 Javascript 操作优化成小块,然后使用requestAnimationFrame(),进行优化。 合成(Compositing) 现在,浏览器已经知道了文档结构、每一个元素的样式,元素的几何信息,绘制的顺序。将这些信息转化成屏幕上像素的过程叫做光栅化。 传统的做法是将可视区域的内容进行光栅化。随着用户滚动页面,不断的光栅化更多的区域。然而对于现代浏览器,有着更复杂的的过程,这个过程被称做合成。 合成是一种将页面拆分成多层的技术,合成线程可以将各个层在不同线程中光栅化,再组合成一个页面。当页面滚动时,如果层已经被光栅化,则会使用已经存在的层合成新的帧,动画则可以通过移动层来实现。 层(Layer) 为了决定层包含哪些元素,主线程需要遍历布局树以找到需要生成的部分。对开发者来说,当某一部分需要用独立的层渲染,可以使用 css 属性will-change让浏览器创建层。 虽然通过分层可以优化浏览器性能,但并不意味着应该给每个元素一个层,过多的层反而影响性能,所以在层的划分上应该具体形况具体分析。 栅格线程与合成线程 当布局树和绘制顺序确定以后,主线程会将这些信息提交给合成线程。合成线程会光栅化各个层。一个层包含的内容可能是一个完整的页面,也可能是页面的部分,所以合成线程将层拆分成许多块,并将它们发送给栅格线程。栅格线程光栅化这些块并将它们存储在 GPU 缓存中。 合成线程可以决定栅格线程光栅块的优先级,这样可以保证用户能看到的部分可以先被光栅化。一个层也会包含多种块以支持类似缩放这样的功能。 当块被光栅化后,合成线程会使用 draw quads 收集这些信息并创建合成帧(Compositor frame)。 Draw quads(绘制图块的指令) 存储在缓存中,包含类似块位置这样的信息,用于描述如何使用块合成页面。 Compositor frame 用于存储表现页面一帧中包含哪些 Draw quads 的集合。 然后一个合成帧被提交给浏览器进程。这时如果浏览器 UI 有变化,或者插件的 UI 有变化时,另一个合成帧就会被创建出来。所以每当有交互发生时,合成线程就会创建更多的合成帧然后通过 GPU 将新的部分渲染出来。 合成的好处在于其独立于主线程。合成线程不需要等待样式计算和 Javascript 代码的运行。这也是为什么合成更适合优化交互性能,但如果布局或者绘制需要重新计算则主线程是必须要参与的。 本质上,浏览器的渲染过程就是将文本转换成图像的过程,而当用户与页面发生交互动作时,则显示新的图像。在这个过程中由渲染进程中的主线程完成计算工作,由合成线程和栅格线程完成图像的绘制工作。
    https://xie.infoq.cn/article/5d36d123bfd1c56688e125ad3 https://www.jianshu.com/p/d616d887953a
经验分享 程序员 微信小程序 职场和发展