一道经典的问题,却是包含了大部分前端知识体系,今天就由我所学所见总结出一些相对应的知识点。
主要知识框架包括:主要包括浏览器相关知识,输入URL后的过程,性能优化三个方面。
审题一下注意到浏览器输入url,所以你应该先了解下浏览器的相关知识。
浏览器的相关知识
1.进程和线程
进程:资源(内存)分配和携带的最小单元,可以包含多个线程。
线程:cpu调度的最小单位,不分配资源,同个进程里的线程共享进程的资源。
2.浏览器包含哪些进程
浏览器是多进程的,而不是单进程,这样可以在其他进程死锁的情况不会影响其他进程正常运行。
Browser进程:主要功能是控制页面级浏览器进程和进程间的交互
GPU进程:用于3D绘制等 配合GUI线程渲染页面
插件进程:每个浏览器插件都会有一个进程,如可观测的优化插件lighthouse等
页面浏览器渲染进程:打开的每个tab都会创建一个,也是本文中的一代目男主。
3.页面浏览器渲染进程
浏览器渲染进程包含很多线程,具体有下面几个重要的。
GUI渲染线程:页面渲染此线程发挥作用,比如在readerTree渲染时,每次宏任务结束也会触发一次。
JS引擎线程:浏览器中著名的js单线程,本文的二代目男主,运行时与GUI渲染线程互斥,event loop中的男主。
事件触发线程:event loop的男二出现,定时器和http线程处理事件结束后 该线程会把它们放到事件队列里。
定时器线程:处理定时器事件,结束时会把回调放到事件队列里。
异步http请求线程:处理http请求事件,结束时会把回调放到事件队列里。
审题一下注意到浏览器输入url,那么我们已经了解浏览器,接下来就了解输入url后浏览器做了什么。
输入url的过程
具体知识点包括1.DNS域名解析成IP,2.建立TCP连接,3.http报文相关,4.缓存策略,5.页面解析渲染,6.JS单线程与event loop。主要这6个方面吧,其中每个知识点里面都会有很多相关的知识点,那么下面让我们一层层揭开输入url过程的面纱。
1.DNS域名解析
大致流程:
如果浏览器有缓存,直接使用浏览器缓存,否则使用本机缓存,再没有的话就是用host
如果本地没有,就向dns域名服务器查询(当然,中间可能还会经过路由,也有缓存等),查询到对应的IP
dns解析会影响白屏时间,但前端代码层面对这方面优化无能为力。
2.建立TCP连接
先介绍下五层网络模型
- 应用层 http请求
- 传输层 tcp 数据包
- 网络层 IP寻址 数据包
- 链路层 封装成帧
- 物理层 利用物理介质传输比特流0/1
主要知识点在三次握手和四次挥手,具体也不多说了,大家肯定都有了解,直接贴一些老铁的好文地址:juejin.cn/post/684490…
3.http报文相关
这边知识点会介绍1.get post的区别,2.跨域,3.报文结构分析,4.http和https协议
1.get post的区别
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
GET参数通过URL传递,POST放在Request body中(http约定)
GET请求在URL中传送的参数是有长度限制的(浏览器限制2K),而POST么有。
2.跨域
关于跨域之前我也总结了比较常用的一些解决方法
3.报文结构分析
报文一般包括了:通用头部,请求/响应头部,请求/响应体
通用头部
这也是开发人员见过的最多的信息,包括如下:
Request Url: 请求的web服务器地址
Request Method: 请求方式(Get、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE)
Status Code: 请求的返回状态码,如200代表成功
Remote Address: 请求的远程服务器地址(会转为IP)其中最常用的请求方法我们在上面对post和get也做了一些讲解,下面重点看下常见的状态码和区间代表的意思,状态码能帮助快速定位问题。
200——表明该请求被成功地完成,所请求的资源发送回客户端
304——自从上次请求后,请求的网页未修改过,请客户端使用本地缓存
400——客户端请求有错(譬如可以是安全模块拦截)
401——请求未经授权
403——禁止访问(譬如可以是未登录时禁止)
404——资源未找到
500——服务器内部错误
503——服务不可用
...
1xx——指示信息,表示请求已接收,继续处理
2xx——成功,表示请求已被成功接收、理解、接受
3xx——重定向,要完成请求必须进行更进一步的操作
4xx——客户端错误,请求有语法错误或请求无法实现
5xx——服务器端错误,服务器未能实现合法的请求
请求/响应头部
请求和响应头部也是分析时常用到的
常用的请求头部(部分):
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:对应服务端的ETag,用来匹配文件内容是否改变(非常精确),http1.1中
Cookie: 有cookie并且同域访问时会自动带上
Connection: 当浏览器与服务器通信时对于长连接如何进行处理,如keep-alive
Host:请求的服务器URL
Origin:最初的请求是从哪里发起的(只会精确到端口),Origin比Referer更尊重隐私
Referer:该页面的来源URL(适用于所有类型的请求,会精确到详细页面地址,csrf拦截常用到这个字段)
User-Agent:用户客户端的一些必要信息,如UA头部等常用的响应头部(部分)
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后有效
ETag:请求变量的实体标签的当前值
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、ETag对应
请求/响应体
一般我们在发起http请求时如果是get方法会放到params里面传输也体现在URL后面?连接的参数,post请求会放到body体内
响应报文的响应体通常是后端在接受到请求返回给前端的数据包,可能是一个HTML页面也有可能是一串json数据等
首先说下http的特点
http 1.0
默认使用的是短连接,也就是说,浏览器没进行一次http操作,就建立一次连接,任务结束就中断连接,譬如每一个静态资源请求时都是一个单独的连接
请求方法只有GET、POST、HEAD
Expires
If-Modified-Since/Last-Modified
http 1.1
默认使用长连接,使用长连接会有这一行Connection: keep-alive,在长连接的情况下,当一个网页打开完成后,客户端和服务端之间用于传输http的tcp连接不会关闭,如果客户端再次访问这个服务器的页面,会继续使用这一条已经建立的连接
Cache-Control
If-None-Match/E-tag
http 2.0
是http的下一代规范
1.多路复用(即一个tcp/ip连接可以请求多个资源)
2.首部压缩(http头部压缩,减少体积)
3.二进制分帧(在应用层跟传送层之间增加了一个二进制分帧层,改进传输性能,实现低延迟和高吞吐量)
4.服务器端推送(服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端)
5.请求优先级(如果流被赋予了优先级,它就会基于这个优先级来处理,由服务器决定需要多少资源来处理该请求。)
https就是安全版本的http,譬如一些支付等操作基本都是基于https的,因为http请求的安全系数太低了。
https与http的区别就是: 在请求前,会建立ssl链接,确保接下来的通信都是加密的,无法被轻易截取分析
一般来说,如果要将网站升级成https,需要后端支持(后端需要申请证书等),然后https的开销也比http要大(因为需要额外建立安全链接以及加密等),所以一般来说http2.0配合https的体验更佳(因为http2.0更快了)
一般来说,主要关注的就是SSL/TLS的握手流程,如下(简述):
1. 浏览器请求建立SSL链接,并向服务端发送一个随机数–Client random和客户端支持的加密方法,比如RSA加密,此时是明文传输。
2. 服务端从中选出一组加密算法与Hash算法,回复一个随机数–Server random,并将自己的身份信息以证书的形式发回给浏览器
(证书里包含了网站地址,非对称加密的公钥,以及证书颁发机构等信息)
3. 浏览器收到服务端的证书后
- 验证证书的合法性(颁发机构是否合法,证书中包含的网址是否和正在访问的一样),如果证书信任,则浏览器会显示一个小锁头,否则会有提示
- 用户接收证书后(不管信不信任),浏览会生产新的随机数–Premaster secret,然后证书中的公钥以及指定的加密方法加密`Premaster secret`,发送给服务器。
- 利用Client random、Server random和Premaster secret通过一定的算法生成HTTP链接数据传输的对称加密key-`session key`
- 使用约定好的HASH算法计算握手消息,并使用生成的`session key`对消息进行加密,最后将之前生成的所有信息发送给服务端。
4. 服务端收到浏览器的回复
- 利用已知的加解密方式与自己的私钥进行解密,获取`Premaster secret`
- 和浏览器相同规则生成`session key`
- 使用`session key`解密浏览器发来的握手消息,并验证Hash是否与浏览器发来的一致
- 使用`session key`加密一段握手消息,发送给浏览器
5. 浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,
session key并利用对称加密算法进行加密4.缓存策略
很多时候,大家倾向于将浏览器缓存简单地理解为“HTTP 缓存”,通常存放在磁盘里“from disk cache”。但事实上,浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:
- Memory Cache
- Service Worker Cache
- HTTP Cache
- Push Cache
其中,“from memory cache”对标到 Memory Cache 类型,表示存在内存中,因为资源比较宝贵,常用来放一些base64图片等。“from ServiceWorker”对标到 Service Worker Cache 类型,必须以 https 协议为前提,会存放一些图片静态资源。至于 Push Cache,这个比较特殊,是 HTTP2 的新特性。
HTTP Cache,缓存可以简单的划分成两种类型:强缓存与协商缓存
区别简述如下:
-
强缓存(
200 from cache)时,浏览器如果判断本地缓存未过期,就直接使用,无需发起http请求 -
协商缓存(
304)时,浏览器会向服务端发起http请求,然后服务端告诉浏览器文件未改变,让浏览器使用本地缓存
对于协商缓存,使用Ctrl + F5强制刷新可以使得缓存无效
强缓存的实现:从 expires 到 cache-control
http 1.0时 过去我们一直用 expires
当服务器返回响应时,在 Response Headers 中将过期时间写入 expires 字段。像这样:
expires: Wed, 11 Sep 2019 16:12:18 GMT可以看到,expires 是一个时间戳,接下来如果我们试图再次向服务器请求资源,浏览器就会先对比本地时间和 expires 的时间戳,如果本地时间小于 expires 设定的过期时间,那么就直接去缓存中取这个资源。
从这样的描述中大家也不难猜测,expires 是有问题的,它最大的问题在于对“本地时间”的依赖。如果服务端和客户端的时间设置可能不同,或者我直接手动去把客户端的时间改掉,那么 expires 将无法达到我们的预期。
考虑到 expires 的局限性,HTTP1.1 新增了 Cache-Control 字段来完成 expires 的任务。
expires 能做的事情,Cache-Control 都能做;expires 完成不了的事情,Cache-Control 也能做。因此,Cache-Control 可以视作是 expires 的完全替代方案。在当下的前端实践里,我们继续使用 expires 的唯一目的就是向下兼容。
cache-control: max-age=31536000如大家所见,在 Cache-Control 中,我们通过 max-age 来控制资源的有效期。max-age 不是一个时间戳,而是一个时间长度。在本例中,max-age 是 31536000 秒,它意味着该资源在 31536000 秒以内都是有效的,完美地规避了时间戳带来的潜在问题。
Cache-Control 相对于 expires 更加准确,它的优先级也更高。当 Cache-Control 与 expires 同时出现时,我们以 Cache-Control 为准。
no-cache和no-store
no-cache 绕开了浏览器:我们为资源设置了 no-cache 后,每一次发起请求都不会再去询问浏览器的缓存情况,而是直接向服务端去确认该资源是否过期(即走我们下文即将讲解的协商缓存的路线)。
no-store 比较绝情,顾名思义就是不使用任何缓存策略。在 no-cache 的基础上,它连服务端的缓存确认也绕开了,只允许你直接向服务端发送请求、并下载完整的响应。
协商缓存:浏览器与服务器合作之下的缓存策略
协商缓存依赖于服务端与浏览器之间的通信。
协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。
如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304。
协商缓存的实现:从 Last-Modified 到 Etag
Last-Modified 是一个时间戳,如果我们启用了协商缓存,它会在首次请求时随着 Response Headers 返回:
Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT随后我们每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:
If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;否则,返回如上图的 304 响应,Response Headers 不会再添加 Last-Modified 字段。
使用 Last-Modified 存在一些弊端,如果文件内容未更改或者更改再复原,也会导致整个文件的时间戳出现误判,服务器并没有正确感知文件的变化。为了解决这样的问题,Etag 作为 Last-Modified 的补充出现了。
Etag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的,反之亦然。因此 Etag 能够精准地感知文件的变化。
Etag 和 Last-Modified 类似,当首次请求时,我们会在响应头里获取到一个最初的标识符字符串,举个🌰,它可以是这样的:
ETag: W/"2a3b-1602480f459"那么下一次请求时,请求头里就会带上一个值相同的、名为 if-None-Match 的字符串供服务端比对了:
If-None-Match: W/"2a3b-1602480f459"Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能,这是它的弊端。因此启用 Etag 需要我们审时度势。正如我们刚刚所提到的——Etag 并不能替代 Last-Modified,它只能作为 Last-Modified 的补充和强化存在。 Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。
大概就是如果命中强缓存就直接从磁盘读出内容,Cache-Control优先级高,http code:200,如果没有强缓存会去检查弱缓存,Etag优先级较高http code:304。
5.页面解析渲染
http请求拿到响应体html文本之后就到了页面解析,大体流程在五个步骤
1. 解析HTML,构建DOM树
2. 解析CSS,生成CSS规则树
3. 合并DOM树和CSS规则,生成render树
4. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
5. 绘制render树(paint/Repaint),绘制页面像素信息
DOM 树:解析 HTML 以创建的是 DOM 树(DOM tree ):渲染引擎开始解析 HTML 文档,转换树中的标签到 DOM 节点,它被称为“内容树”。
CSSOM 树:解析 CSS(包括外部 CSS 文件和样式元素)创建的是 CSSOM 树。CSSOM 的解析过程与 DOM 的解析过程是并行的。
渲染树:CSSOM 与 DOM 结合,之后我们得到的就是渲染树(Render tree )。
布局渲染树:从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标,我们便得到了基于渲染树的布局渲染树(Layout of the render tree)。
绘制渲染树: 遍历渲染树,每个节点将使用 UI 后端层来绘制。整个过程叫做绘制渲染树(Painting the render tree)。
这里Layout和Repaint的概念是有区别的:
-
Layout,也称为Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树
-
Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了
什么会引起回流?
1.页面渲染初始化
2.DOM结构改变,比如删除了某个节点
3.render树变化,比如减少了padding
4.窗口resize
5.最复杂的一种:获取某些属性,引发回流,
很多浏览器会对回流做优化,会等到数量足够时做一次批处理回流,
但是除了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等属性。无法避免则将它们缓存到变量
-
将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高
6.JS单线程与event loop
通常js文件都在body的最下面的script标签内,当运行到js的时候,会创建一个全局执行上下文并在执行栈底,之前我也总结过执行上下文的相关知识
然后当我们遇到异步事件也会有特定的处理规律也就是event loop,之前我也总结过event loop知识
性能优化
关于性能优化我也总结了一篇文章,从webpack打包和页面渲染两个方面入手,对减小http请求数据包大小和减少http请求数量两个方面着手实践