一道面试题

浏览器中输入 www.baidu.com 直到看到页面之间发生了什么?

今天就说说根据这道题梳理自己的前端知识!

解析 URL

解释 URL 是浏览器的活,所以首先要搞明白浏览器的工作方式。
浏览器是多进程的,有一个主控进程,进程可能包括主控进程,插件进程,GPU,tab 页(浏览器内核)等等,主要包括:

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

浏览器渲染进程是多线程的,它有主要几大类子线程

  • GUI 线程
  • JS 引擎线程
  • 事件触发线程
  • 定时器线程
  • 网络请求线程

输入 URL 后,从浏览器会进行解析,URL 一般包括几大部分:

  • protocol,协议头,譬如有 http,https 等
  • host,主机域名或 IP 地址
  • port,端口号
  • path,目录路径
  • query,即查询参数
  • fragment,即 # 后的 hash 值,一般用来定位到某个位置

每次网络请求时都需要开辟单独的线程进行,览器会根据解析出得协议,开辟一个网络线程,前往请求资源.

http 缓存

网络请求之前会先都差缓存,HTTP 缓存有多种规则,根据是否需要重新向服务器发起请求来分类,将其分为强制缓存,对比缓存。

  • 强缓存(200 from cache)时,浏览器如果判断本地缓存未过期,就直接使用,无需发起 http 请求

  • 对比缓存(304)时,浏览器会向服务端发起 http 请求,然后服务端告诉浏览器文件未改变,让浏览器使用本地缓存,可以使用 Ctrl + F5 强制刷新可以使得对比缓存无效

http1.0 中的缓存控制:

  • Pragma:严格来说,它不属于专门的缓存控制头部,但是它设置 no-cache 时可以让本地强缓存失效(属于编译控制,来实现特定的指令,主要是因为兼容 http1.0,所以以前又被大量应用)
  • Expires:服务端配置的,属于强缓存,用来控制在规定的时间之前,浏览器不会发出请求,而是直接使用本地缓存,注意,Expires 一般对应服务器端时间,如 Expires:Fri, 30 Oct 1998 14:19:41
  • If-Modified-Since/Last-Modified:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是 If-Modified-Since,而服务端的是 Last-Modified,它的作用是,在发起请求时,如果 If-Modified-Since 和 Last-Modified 匹配,那么代表服务器资源并未改变,因此服务端不会返回资源实体,而是只返回头部,通知浏览器可以使用本地缓存。Last-Modified,顾名思义,指的是文件最后的修改时间,而且只能精确到 1s 以内

http1.1 中的缓存控制:

  • Cache-Control:缓存控制头部,有 no-cache、max-age 等多种取值

    • Max-Age:服务端配置的,用来控制强缓存,在规定的时间之内,浏览器无需发出请求,直接使用本地缓存,注意,Max-Age 是 Cache-Control 头部的值,不是独立的头部,譬如 Cache-Control: max-age=3600,而且它值得是绝对时间,由浏览器自己计算
  • If-None-Match/E-tag:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是 If-None-Match,而服务端的是 E-tag,同样,发出请求后,如果 If-None-Match 和 E-tag 匹配,则代表内容未变,通知浏览器使用本地缓存,和 Last-Modified 不同,E-tag 更精确,它是类似于指纹一样的东西,基于 FileEtag INode Mtime Size 生成,也就是说,只要文件变,指纹就会变,而且没有 1s 精确度的限制。

强缓存判断依据

1
2
(http1.1)Cache-Control => Cache-Control: max-age = 35600
(http1.0)Expires => 服务器端的时间

cache-control 中的 max-age 保存一个相对时间。例如 Cache-Control: max-age = 35600,表示浏览器收到文件后,缓存在 35600s 内均有效。如果同时存在 cache-control 和 Expires ,浏览器总是优先使用 cache-control。
Max-Age 相比 Expires,Expires 使用的是服务器端的时间,但是有时候会有这样一种情况-客户端时间和服务端不同步。所以一般 http1.1 后不推荐使用 Expires。

对比缓存判断依据

1
2
(http1.1)E-tag/If-None-Match
(http1.0)Last-Modified/If-Modified-Since

last-modified 是第一次请求资源时,服务器返回的字段,表示最后一次更新的时间。下一次浏览器请求资源时就发送 if-modified-since 字段。服务器用本地 Last-modified 时间与 if-modified-since 时间比较,如果不一致则认为缓存已过期并返回新资源给浏览器;如果时间一致则发送 304 状态码,让浏览器继续使用缓存。

E-tag:资源的实体标识(哈希字符串),当资源内容更新时,E-tag 会改变。服务器会判断 E-tag 是否发生变化,如果变化则返回新资源,否则返回 304。

如果同时带有 E-tag 和 Last-Modified,服务端会优先检查 E-tag

在浏览器接收到服务器响应后,会检测响应头部(Header),如果有 nEtag 字段,那么浏览器就会将本次缓存写入硬盘中。

浏览器优先强缓存,如果设置了 no-cache 会走协商缓存

开启线程发出请求

  • dns 查询

dns 是通常是完成域名到 ip 的映射,大致流程:

  • 如果浏览器有缓存,直接使用浏览器缓存,否则使用本机缓存,再没有的话就是用 host
  • 如果本地没有,就向 dns 域名服务器查询(当然,中间可能还会经过路由,也有缓存等),查询到对应的 IP

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

1
2
3
4
5
a 标签的 DNS 预解析 默认是开启的 但是当 使用 https 的时候是默认关闭的需要手动开启 a 标签的预解析
<meta http-equiv="x-dns-prefetch-control" content="on">

开启 dns 预解析
<link rel="dns-prefetch" href="IP地址">
  • tcp/ip 请求

首先会建立 tcp 连接

  • 建立链接(三次握手)
1
2
3
客户端发个请求“开门呐,我要进来”给服务器
服务器发个“进来吧,我去给你开门”给客户端
客户端有很客气的发个“谢谢,我要进来了”给服务器

建立连接也会断开连接只是不是这时候断开,先提前说下

  • 断开连接(四次挥手)
1
2
3
4
客户端发个“时间不早了,我要走了”给服务器,等服务器起身送他
服务器听到了,发个“我知道了,那我送你出门吧”给客户端,等客户端走
服务器把门关上后,发个“我关门了”给客户端,然后等客户端走(尼玛~矫情啊)
客户端发个“我知道了,我走了”,之后自己就走了

由于 tcp/ip 对链接有并发的控制,所以有甚多针对请求的优化,比如合并请求的雪碧图。
关于这里有涉及到五层网络协议,就是从客户端发出 http 请求到服务器接收,中间会经过一系列的流程。

1
2
3
4
5
应用层(dns,http) DNS 解析成IP并发送 http 请求
传输层(tcp/ip)建立连接
网络层(ip)IP 寻址
数据链层(ppp)封装成二进制帧
物理层 物理传输

其实也有一个完整的 OSI 七层框架,与之相比,多了会话层、表示层。

浏览器向服务器发送 HTTP 请求。

  • http 报文结构

报文一般包括了:通用头部,请求/响应头部,请求/响应体

  • 通用头部
1
2
3
4
5
Request Url: 请求的web服务器地址
Request Method: 请求方式
(Get、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE)
Status Code: 请求的返回状态码,如200代表成功
Remote Address: 请求的远程服务器地址(会转为IP)

code 状态码

1
2
3
4
5
1xx——指示信息,表示请求已接收,继续处理
2xx——成功,表示请求已被成功接收、理解、接受
3xx——重定向,要完成请求必须进行更进一步的操作
4xx——客户端错误,请求有语法错误或请求无法实现
5xx——服务器端错误,服务器未能实现合法的请求

请求/响应头部

常用的请求头部(部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Accept: 接收类型,表示浏览器支持的MIME类型(对标服务端返回的 Content-Type )
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:对应服务端的 E-tag,用来匹配文件内容是否改变(非常精确),http1.1 中
Cookie: 有 cookie 并且同域访问时会自动带上
Connection: 当浏览器与服务器通信时对于长连接如何进行处理,如 keep-alive
Host:请求的服务器 URL
Origin:最初的请求是从哪里发起的(只会精确到端口),Origin 比 Referer 更尊重隐私
Referer:该页面的来源URL(适用于所有类型的请求,会精确到详细页面地址,csrf拦截常用到这个字段)
User-Agent:用户客户端的一些必要信息,如 UA 头部等

常用的响应头部(部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
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 后有效
E-tag:请求变量的实体标签的当前值
Set-Cookie:设置和页面关联的 cookie,服务器通过这个头部把 cookie 传给客户端
Keep-Alive:如果客户端有 keep-alive,服务端也会有响应(如 timeout=38 )
Server:服务器的一些相关信息

一般来说,请求头部和响应头部是匹配分析的。

譬如,请求头部的 Accept 要和响应头部的 Content-Type 匹配,否则会报错

譬如,跨域请求时,请求头部的 Origin 要匹配响应头部的 Access-Control-Allow-Origin,否则会报跨域错误

譬如,在使用缓存时,请求头部的 If-Modified-Since、If-None-Match 分别和响应头部的 Last-Modified、E-tag 对应

服务器接收到请求

  • 负载均衡

对于大型的项目,由于并发访问量很大,所以往往一台服务器是吃不消的,所以一般会有若干台服务器组成一个集群,然后配合反向代理实现负载均衡。用户发起的请求都指向调度服务器(反向代理服务器,譬如安装了 nginx 控制负载均衡),然后调度服务器根据实际的调度算法,分配不同的请求给对应集群中的服务器执行,然后调度器等待实际服务器的 HTTP 响应,并将它反馈给用户

  • 后台的处理

  • cookie
    cookie 是浏览器的一种本地存储方式,一般用来帮助客户端和服务端通信的,常用来进行身份校验,结合服务端的 session 使用.
    由于在同域名的资源请求时,浏览器会默认带上本地的 cookie,针对这种情况,在某些场景下是需要优化,例如:

  • 将静态资源分组,分别放到不同的子域名下

  • 而子域名请求时,是不会带上父级域名的 cookie 的,所以就避免了浪费

说到了多域名拆分,这里再提一个问题,那就是:

在移动端,如果请求的域名数过多,会降低请求速度(因为域名整套解析流程是很耗费时间的,而且移动端一般带宽都比不上 pc)此时就需要用到一种优化方案:dns-prefetch(让浏览器空闲时提前解析 dns 域名,不过也请合理使用,勿滥用)

说到 cookie 就要把 localStorage,sessionStorage 区分下:

1
2
3
4
5
6
7
8
9
cookie 数据始终在同源的 http 请求中携带(即使不需要),即 cookie 在浏览器和服务器间来回传递。而 sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存。

cookie 数据还有路径(path)的概念,可以限制 cookie 只属于某个路径下。

存储大小限制也不同,cookie 数据不能超过 4k,同时因为每次 http 请求都会携带cookie,所以 cookie 只适合保存很小的数据,如会话标识。sessionStorage 和 localStorage 虽然也有存储大小的限制,但比 cookie 大得多,可以达到5M或更大。

数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭前有效,自然也就不可能持久保持;localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie只在设置的 cookie 过期时间之前一直有效,即使窗口或浏览器关闭。

作用域不同,sessionStorage 不在不同的浏览器窗口中共享,即使是同一个页面;localStorage 在所有同源窗口中都是共享的;cookie 也是在所有同源窗口中都是共享的。
  • gzip 压缩

gzip 是 GNU zip 的缩写,它是一个 GNU 自由软件的文件压缩程序。一般服务器都会开启 gzip 这样可以减少带宽消耗。但是 HTTP 压缩需要成本。Web 服务器获得需要的内容,然后压缩它,最后将它发送到客户端。如果内容不能被进一步压缩,你只是在浪费 CPU 做无意义的任务,采用 HTTP 压缩已经被过压缩的东西并不能使它更小。事实上,添加标头,压缩字典,并校验响应体实际上使它变得更大。

  • 长连接与短连接

首先看 tcp/ip 层面的定义:

  • 长连接:一个 tcp/ip 连接上可以连续发送多个数据包,在 tcp 连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持(类似于心跳包)
  • 短连接:通信双方有数据交互时,就建立一个 tcp 连接,数据发送完成后,则断开此 tcp 连接

然后在 http 层面:

  • http1.0 中,默认使用的是短连接,也就是说,浏览器每进行一次 http 操作,就建立一次连接,任务结束就中断连接,譬如每一个静态资源请求时都是一个单独的连接
  • http1.1 起,默认使用长连接,使用长连接会有这一行 Connection: keep-alive,在长连接的情况下,当一个网页打开完成后,客户端和服务端之间用于传输 http 的 tcp 连接不会关闭,如果客户端再次访问这个服务器的页面,会继续使用这一条已经建立的连接

注意: keep-alive 不会永远保持,它有一个持续时间,一般在服务器中配置(如 apache),另外长连接需要客户端和服务器都支持时才有效

  • https

简单来看,https 与 http 的区别就是: 在请求前,会建立 ssl 链接,确保接下来的通信都是加密的,无法被轻易截取分析。如果要将网站升级成 https,需要后端支持(后端需要申请证书等),然后 https 的开销也比 http 要大(因为需要额外建立安全链接以及加密等),所以一般来说 http2.0 配合 https 的体验更佳(因为 http2.0 更快了)SSL/TLS 的握手流程握手流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 浏览器建立 SSL 连接,像服务端 发送一个随机数和加密方法
2. 服务端选取加密方法回复一个随机数并将自己的证书发送过去
3. 浏览器收到证书后
- 验证证书合法性
- 用户街道证书后生成新的随机数,然后证书中的公钥以及指定的加密方法加密发送
- 通过一定的算法生成 HTTP 链接数据传输的对称加密 key
- 使用约定好的算法计算握手消息,并使用生成的 key 对消息进行加密,最后将之前生成的所有信息发送给服务端。
4. 服务端收到浏览器的回复
- 利用已知的加解密方式与自己的私钥进行解密
- 和浏览器相同规则生成 key
- 使用 key 解密浏览器发来的握手消息,并验证Hash是否与浏览器发来的一致
- 使用 key 加密一段握手消息,发送给浏览器
5. 浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束
  • http 2.0

http2.0 它相当于是 http 的下一代规范,与 http1.1 的显著不同点:

  • http1.1 中,每请求一个资源,都是需要开启一个 tcp/ip 连接的,所以对应的结果是,每一个资源对应一个 tcp/ip 请求,由于 tcp/ip 本身有并发数限制,所以当资源一多,速度就显著慢下来
  • ttp2.0 中,一个 tcp/ip 请求可以请求多个资源,也就是说,只要一次 tcp/ip 请求,就可以请求若干个资源,分割成更小的帧请求,速度明显提升。

http2.0 的一些特性:

  • 多路复用(即一个 tcp/ip 连接可以请求多个资源)
  • 首部压缩(http 头部压缩,减少体积)
  • 二进制分帧(在应用层跟传送层之间增加了一个二进制分帧层,改进传输性能,实现低延迟和高吞吐量)
  • 服务器端推送(服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端)
  • 请求优先级(如果流被赋予了优先级,它就会基于这个优先级来处理,由服务器决定需要多少资源来处理该请求。)

注意:HTTP2.0 的多路复用和 HTTP1.1 中的长连接复用的区别就是:

HTTP/1.1 Pipeling 解决方式为,若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞。

HTTP/2 多个请求可同时在一个连接上并行执行。某个请求任务耗时严重,不会影响到其它连接的正常执行;

  • 浏览器接收响应

  • 构建渲染树

浏览器进行解析渲染呈现给用户。整个过程涉及两个方面:解析和渲染。在渲染页面之前,需要构建 DOM 树和 CSSOM 树。

  • HTML 解析,构建 DOM

解析 HTML 到构建出 DOM 当然过程可以简述如下:

Bytes → characters → tokens → nodes → DOM

  • 生成 CSS 规则

CSS 规则树的生成也是类似。简述为:

Bytes → characters → tokens → nodes → CSSOM

  • 渲染

布局是由 CPU 处理的,而绘制则是由 GPU 完成的,GPU 会对我们的渲染层作缓存,如果我们把那些一直发生大量重排重绘的元素提取出来,单独触发一个渲染层,那样这个元素不就不会“连累”其他元素一块重绘。
Video 元素、WebGL、Canvas、CSS3 3D、CSS 滤镜、z-index 大于某个相邻节点的元素都会触发新的 Layer,如果图层中某个元素需要重绘,那么整个图层都需要重绘。比如一个图层包含很多节点,其中有个 gif 图,gif 图的每一帧,都会重回整个图层的其他节点,然后生成最终的图层位图。所以这需要通过特殊的方式来强制 gif 图属于自己一个图层,就是给某个元素加上下面的样式:

1
2
transform: translateZ(0);
backface-visibility: hidden;

也会触发渲染层,把容易触发重排重绘的元素单独触发渲染层,让它与那些“静态”元素隔离,让 GPU 分担更多的渲染工作,我们通常把这样的措施成为硬件加速,或者是 GPU 加速。

1
2
3
4
5
6
7
Style:该区域为样式计算阶段,浏览器会根据选择器(就是CSS选择器,如.td)计算出哪些节点应用哪些CSS规则,然后计算出每个节点的最终样式并应用到节点上。

Layout:该区域为布局计算阶段,浏览器会在该过程中根据节点的样式规则来计算它要占据的空间大小以及在屏幕中的位置。

Paint:该区域为绘制阶段,浏览器会先创建绘图调用的列表,然后填充像素。绘制阶段会涉及到文本、颜色、图像、边框和阴影,基本上包括了每个可视部分。绘制一般是在多个图层(用过Photoshop等图片编辑软件的童鞋一定很眼熟图层这个词,这里的图层的含义其实是差不多的)上完成的。

Composite:该区域为合成阶段,浏览器将多个图层按照正确顺序绘制到屏幕上。

如果动态修改了 DOM 或 CSS,就会重新布局(Layout)或渲染(Repaint)这里 Layou t 和 Repaint 的概念是有区别的:

  • Layout,也称为 Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树

  • Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了

什么会引起回流?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1.页面渲染初始化

2.DOM结构改变,比如删除了某个节点

3.render树变化,比如减少了padding

4.窗口resize

5.改变字体大小会引发回流

6.最复杂的一种:获取某些属性,引发回流
很多浏览器会对回流做优化,会等到数量足够时做一次批处理回流,但是除了render树的直接变化,当获取一些属性时,浏览器为了获得正确的值也会触发回流,这样使得浏览器优化无效,包括
(1) offset(Top/Left/Width/Height)
(2) scroll(Top/Left/Width/Height)
(3) cilent(Top/Left/Width/Height)
(4) width,height
(5) 调用了getComputedStyle()或者IE的currentStyle

回流一定伴随着重绘,重绘却可以单独出现

所以一般会有一些优化方案,如:

  • 减少逐项更改样式,最好一次性更改 style,或者将样式定义为 class 并一次性更新
  • 避免循环操作 dom,创建一个 documentFragment 或 div,在它上面应用所有 DOM 操作,最后再把它添加到 window.document
  • 避免多次读取 offset 等属性。无法避免则将它们缓存到变量
  • 将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高

最后如果想要知道每个 CSS 属性将会对哪个阶段产生怎样的影响,请去 CSS Triggers,该网站详细地说明了每个 CSS 属性会影响到哪个阶段。

  • 关闭 TCP 连接或继续保持连接

通过四次挥手关闭连接