Back To Blog

从输入URL到页面加载的过程

September 17, 201829 min read

Overview

通过对这道题的分析,希望梳理出一个前端向的知识体系 💃 主要包括:

  • 从浏览器接收 url 到开启网络请求线程
  • 开启网络线程到发出一个完整的 http 请求
  • 从服务器接收到请求到对应后台接收到请求
  • 后台和前台的 http 交互
  • 缓存问题,http 缓存总结
  • 解析页面流程
  • CSS 的可视化格式模型
  • JS 引擎解析过程

从浏览器接收 url 到开启网络请求线程

线程与进程

进程和线程的对比网上大多是概念性的解释,要真正理解进程与线程中的时间片和其他精彩的“调度概念”之前, 建立一个类比,首先来说明线程和进程是如何工作的。

作为房子

使用常规的日常对象 - 房子来对进程和线程进行类比。
房子实际上是一个容器,具有某些属性(例如占地面积,卧室数量等)。房子本身并没有主动做 任何事情 - 这是一个被动的对象。这实际上是一个进程。
住在房子里的人是活跃的对象 - 他们是使用各种房间,看电视,做饭,洗澡等等。线程的行为方式。

单线程

如果你住在自己的房子里,那么你可以在任何时间做任何事,因为房子里没有其他人。 你可以打开电视,使用洗手间,吃晚餐等等,只需要继续操作即可。

多线程

当把另一个人加入房子时,你不能在任何一个点上进入洗手间,你需要先检查一下,洗手间有没有人。
如果有两个负责任的成年人住在一个 ​​ 房子里,一般来说可以合理地对“安全”松懈,你知道另一个成年人会尊重你的空间,不会让厨房着火等等。 现在,把几个孩子扔进去,事情变得更加复杂。

回到线程与进程

在 macOS 系统中,可以打开活动监视器查看后台进程与线程,和内存资源信息和 CPU 占有率。

进程
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。
线程
由于进程对于 CPU 的使用是轮流的,那么就存在进程的切换,但是由于现在的程序都比较大,切换的开销很大会浪费 CPU 的资源,于是就发明了线程,把一个大的进程分解成多个线程共同执行。
区别

  • 进程是操作系统分配资源的最小单位,线程是程序执行的最小单位。
  • 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
  • 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)。
  • 调度和切换:线程上下文切换比进程上下文切换要快得多。

浏览器是多进程的

浏览器是多进程的,有一个主控进程,以及每一个 tab 页面都会新开一个进程(某些情况下多个 tab 会合并进程)。 如图,查看 chrome 任务管理器。

浏览器进程包括

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

浏览器内核是多线程

浏览器内核

简单来说浏览器内核是通过取得页面内容、整理信息(应用 CSS)、计算和组合最终输出可视化的图像结果,通常也被称为渲染引擎。 从上面我们可以知道,Chrome 浏览器为每个 tab 页面单独启用进程,因此每个 tab 网页都有由其独立的渲染引擎实例。
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:

  • GUI 渲染线程(图形用户界面)
  • JavaScript 引擎线程
  • 事件触发线程
  • 定时触发器线程
  • 异步 http 请求线程

GUI 渲染线程

GUI渲染线程负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。
GUI 渲染线程与 JS 引擎线程是互斥的 在 Javascript 引擎运行脚本期间,GUI 渲染线程都是处于挂起状态的,也就是说被”冻结”了.

JavaScript 引擎线程

Javascript引擎可以称为 JS 内核,主要负责处理 Javascript 脚本程序,例如 V8 引擎。Javascript 引擎线程理所当然是负责解析 Javascript 脚本,运行代码。
一个 Tab 页(renderer 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序。

Javascript 是单线程的

Javascript 是单线程的, 那么为什么 Javascript 要是单线程的? 这是因为 Javascript 这门脚本语言诞生的使命所致:JavaScript 为处理页面中用户的交互,以及操作 DOM 树、CSS 样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。 如果 JavaScript 是多线程的方式来操作这些 UI DOM,则可能出现 UI 操作的冲突; 如果 Javascript 是多线程的话,在多线程的交互下,处于 UI 中的 DOM 节点就可能成为一个临界资源, 假设存在两个线程同时操作一个 DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。 当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,Javascript 在最初就选择了单线程执行。

事件触发线程

当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。这些事件可以是当前执行的代码块如定时任务、 也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于 JS 的单线程关系所有这些事件都得排队等待 JS 引擎处理。

定时触发器线程

浏览器定时计数器并不是由 JavaScript 引擎计数的, 因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
setInterval 与 setTimeout 所在线程,W3C 在 HTML 标准中规定,规定要求 setTimeout 中低于 4ms 的时间间隔算为 4ms。

异步 http 请求线程

在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。

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

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

开启网络线程到发出一个完整的 http 请求

DNS 查询得到 IP

  1. 如果输入的是域名,需要进行 DNS 解析成 IP,查询步骤:
  2. 浏览器缓存
  3. 本机缓存
  4. hosts 文件
  5. 路由器缓存
  6. ISP DNS 缓存
  7. DNS 递归查询(可能存在负载均衡导致每次 IP 不一样)

TCP/IP 请求

TCP 三次握手

Client                                Server
   |                                     |
   | ---------- SYN ---------------->    |  (1. SYN)
   |                                     |
   | <--------- SYN/ACK --------------    |  (2. SYN-ACK)
   |                                     |
   | ---------- ACK ---------------->    |  (3. ACK)
   |                                     |
  • SYN (Synchronize) 客户端发送一个 SYN 包给服务器,表示客户端希望开始一个连接。
  • SYN-ACK (Synchronize-Acknowledge) 服务器收到 SYN 包后,回应一个 SYN-ACK 包,表示同意连接并且同步连接信息。
  • ACK (Acknowledge) 客户端收到 SYN-ACK 包后,再发送一个 ACK 包,表示确认连接成功。

TCP 四次挥手

Client                                Server
   |                                     |
   | ---------- FIN ---------------->    |  (1. FIN)
   |                                     |
   | <--------- ACK ------------------    |  (2. ACK)
   |                                     |
   | <--------- FIN ------------------    |  (3. FIN)
   |                                     |
   | ---------- ACK ---------------->    |  (4. ACK)
   |                                     |
  • FIN (Finish) 客户端发送一个 FIN 包给服务器,表示客户端希望关闭连接。
  • ACK (Acknowledge) 服务器收到 FIN 包后,回应一个 ACK 包,表示确认收到关闭连接请求,但还未准备好关闭。
  • FIN (Finish) 服务器准备好关闭连接时,发送一个 FIN 包给客户端,表示服务器也希望关闭连接。
  • ACK (Acknowledge) 客户端收到 FIN 包后,回应一个 ACK 包,表示确认关闭连接。

GET/POST 区别

GET 和 POST 本质上就是 TCP 链接,并无差别。但是由于 HTTP 的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
GET 产生一个 TCP 数据包,POST 产生两个 TCP 数据包。
对于 GET 方式的请求,浏览器会把 http header 和 data 一并发送出去,服务器响应 200(返回数据)
而对于 POST,浏览器先发送 header,服务器响应 100 continue,浏览器再发送 data,服务器响应 200 ok(返回数据)

TCP/IP 协议栈

从服务器接收到请求到对应后台接收到请求

负载均衡

用户发起的请求都指向调度服务器(反向代理服务器,譬如安装了 nginx 控制负载均衡),然后调度服务器根据实际的调度算法,分配不同的请求给对应集群中的服务器执行,然后调度器等待实际服务器的 HTTP 响应,并将它反馈给用户。

后台的处理

  • 后端是有统一的验证的,如安全拦截,跨域验证等。如不符合规则,直接返回相应的 http 报文(如拒绝请求等)
  • 当验证通过后,进入实际的后台代码,此时是程序接收到请求,然后执行(譬如查询数据库,大量计算等等)
  • 等程序执行完毕后,就会返回一个 http 响应包(会经过多层封装),将这个包从后端发送到前端,完成交互

后台和前台的 http 交互

http 缓存总结

解析页面流程

渲染机制

浏览器内核拿到内容后,渲染步骤大致可以分为以下几步:

  1. 解析 HTML,构建 DOM 树
  2. 解析 CSS,生成 CSS 规则树
  3. 合并 DOM 树和 CSS 规则,生成 render 树
  4. 布局 render 树(Layout/reflow),负责各元素尺寸、位置的计算
  5. 绘制 render 树(paint),绘制页面像素信息
  6. 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成(composite),显示在屏幕上

在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢。
当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。 并且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM。

Load 和 DOMContentLoaded 区别

Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。
DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载。

图层

一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用。 通过以下几个常用属性可以生成新图层

  • 3D 变换:translate3d、translateZ
  • will-change
  • video、iframe 标签
  • 通过动画实现的 opacity 动画转换
  • position: fixed

更多可参考简单图层与复合图层

重绘(Repaint)和回流(Reflow)

重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大。

  • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘
  • 回流是布局或者几何属性需要改变就称为回流。

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流。

什么会引起回流

  1. 页面渲染初始化
  2. DOM 结构改变,比如删除了某个节点
  3. render 树变化,比如减少了 padding
  4. 窗口 resize
  5. 最复杂的一种:获取某些属性,引发回流
    很多浏览器会对回流做优化,会等到数量足够时做一次批处理回流, 但是除了 render 树的直接变化,当获取一些属性时,浏览器为了获得正确的值也会触发回流,这样使得浏览器优化无效,包括
    • offset(Top/Left/Width/Height)
    • scroll(Top/Left/Width/Height)
    • cilent(Top/Left/Width/Height)
    • width,height
    • 调用了 getComputedStyle()或者 IE 的 currentStyle

减少重绘和回流

  • 使用 translate 替代 top
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改 100 次,然后再把它显示出来
  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免 DOM 深度过深
  • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层。

CSS 的可视化格式模型

CSS 的可视化格式模型就是规定了浏览器在页面中如何处理文档树
CSS 中规定每一个元素都有自己的盒子模型(相当一规定了这个元素如何显示), 然后可视化格式模型则是把这些盒子模型按照规则摆放到页面上,也就是如何布局, 换句话说,盒子模型规定了怎么在页面上摆放盒子,盒子的相互作用等等。

定位机制

CSS 三种定位机制:普通流、浮动流、绝对定位

包含块(Containing Block)

一个元素的 box 的定位和尺寸,会与某一矩形框有关,这个框就称之为包含块。
元素会为它的子孙元素创建包含块,但是,并不是说元素的包含块就是它的父元素,元素的包含块与它的祖先元素的样式等有关系
譬如:

  • 根元素是最顶端的元素,他没有父节点,它的包含块就是初始化包含块;
  • static 和 relative 的包含块由他最近的块级、单元格或者行内块祖先元素的内容框(content)创建;
  • fixed 的包含块就是当前可视窗口;
  • absolute 的包含块由他最近的 position 属性值不为 static 的祖先元素创建:
    • 如果其祖先元素是行内元素,则包含块取决于其祖先元素的 direction 特性;
    • 如果祖先元素不是行内元素,那么包含块的区域应该是祖先元素的内边距边界。

控制框(Controlling Box)

块级元素和块框以及行内元素和行框的相关概念

块框

  • 块级元素会生成一个块框(Block Box),块框会占据一整行,用来包含子 box 和生成的内容
  • 块框同时也是一个块包含框(Containing Box),里面要么只包含块框,要么只包含行内框(不能混杂)
  • 如果块框内部有块级元素也有行内元素,那么行内元素会被匿名块框包围

如果一个块框在其中包含另外一个块框,那么我们强迫它只能包含块框,因此其它文本内容生成出来的都是匿名块框(而不是匿名行内框)

行内框

  • 一个行内元素生成一个行内框
  • 行内元素能排在一行,允许左右有其它元素

display 属性的影响

  • block,元素生成一个块框;
  • inline,元素产生一个或多个的行内框;
  • inline-block,元素产生一个行内级块框,行内块框的内部会被当做块框来格式化,而此元素本身会被当作行内级框来格式化(这也是为什么会产生 BFC);
  • none,不生成框,不再格式化结构中,而另一个 visibility:hidden 则会产生一个不可见的框

总结

如果一个框里,有一个块级元素,那么这个框里的内容都会被当作块框来进行格式化,因为只要出现了块级元素,就会将里面的内容分成几块,每一块独占一行(出现行内可以用匿名块框解决)
如果一个框里,没有任何块级元素,那么这个框里的内容会被当成行内框来格式化,因为里面的内容时按照顺序成行的排列。

FC(Formatting Context)

FC 即格式化上下文,它定义框内部的元素渲染规则,比较抽象,譬如:

  • FC 就像是一个大箱子,里面装有很多元素;
  • 箱子可以隔开里面的元素和外面的元素(所以外部并不会影响 FC 内部的渲染)
  • 内部的规则可以是:如何定位、宽高计算、margin 折叠等等

不同类型的框参与的 FC 类型不同,譬如块级框对应 BFC,行内框对应 IFC
注意:并不是说所有的框都会产生 FC,而是符合特定的条件才会产生,只有产生了对应的 FC 后才会应用对应的 FC 渲染规则

BFC 规则

在块格式化上下文中,每一个元素左外边与包含块的左边解除(对于从右到左的格式化,右外边接触右边),即使存在浮动也是如此(所以浮动元素正常会直接贴近它的包含块的左边,与普通元素重合),除非这个元素也创建了一个新的 BFC;

BFC 特点

  • 内部 box 在垂直方向,一个接一个的放置;
  • box 的垂直方向由 margin 决定,属于同一个 BFC 的两个 box 间的 margin 会重叠
  • BFC 区域不会与 float box 重叠(可用于排版)
  • BFC 就是页面上的一个隔离的独立容器,容器里的子元素不会影响到外面的元素,反之也是如此
  • 计算 BFC 的高度时,浮动元素也参与计算(不会浮动塌陷如 overflow:hidden 清除浮动就是这个原理)

如何触发 BFC

  • 根元素;
  • float 属性不为 none
  • position 为 absolute 或 fixed
  • display 为 inline-block、flex、inline-flex、table、table-cell、table-caption
  • overflow 不为 visible
  • display:table,本身不会产生 BFC,但是他会产生匿名框(包含 display:table-cell 的框),而这个匿名框产生 BFC。

IFC 规则

在行内格式化上下文中,框一个接一个地水平排列,起点是包含块的顶部。水平方向上的 margin,border 和 padding 在框之间得到保留,框在垂直方向上可以以不同的方式对齐; 它们的顶部或底部对齐,或根据其中文字的基线对齐

行框

包含那些框的长方形区域,会形成一行,叫做行框。行框的宽度有它的包含块和其中的浮动元素决定,高度的确定由行高度计算规则决定;

行框的规则

  • 如果几个行内框在水平方向上无法放入一个行框内,它们可以分配在两个或多个垂直堆叠的行框中(即行内框的分割)
  • 行框在堆叠是没有垂直方向上的分割且永远不重叠;
  • 行框的高度总是足够容纳所包含的所有框,不过他可能高于他包含的最高的框(例如,框对齐会引起基线对齐)
  • 行框的左边接触到其包含块的左边,右边接触到其包含块的右边。

总结

  1. 行内元素总是会应用 IFC 渲染规则;
  2. 行内元素会应用 IFC 规则渲染,譬如 text-align 可以用来居中等;
  3. 块框内部对于文本这类的匿名元素,会产生匿名行框包围,而行框内部就应用 IFC 渲染规则
  4. 行内框内部,对于那些行内元素,一样应用 IFC 渲染规则;
  5. 另外,inline-block,会在元素外层产生 IFC(所以这个元素可以通过 text-align 水平居中),当然,它的内部则按照 BFC 规则渲染
© MiaMia2025. All rights reserved.