输入url发生了什么你都不懂,还和我讲性能优化

1,677 阅读16分钟

前言

老生常谈的一个面试题,输入url后到网页展示出来都发生了什么,简单的一个问题,却涉及到了我们前端的各个方面,以及我们常见的性能优化问题。下面我们就来一起看一下吧。

持续更新中,建议收藏~

整个过程

先简单介绍一个整个流程

  • 首先先通过 Dns 将URL解析为对应的Ip地址

  • 然后通过Ip地址 建立TCP链接

  • 发送http请求

  • 处理http请求,http返回客户端数据

  • 拿到响应数据进行解析

  • 关闭tcp 连接

下面就按照这个过程把每一点展开说一下

DNS 解析

先来简单介绍一下dns,在我们输入url请求的时候,会通过DNS服务器将域名翻译为对应的IP地址。

全球一共有13组根服务器来用来翻译全部的域名,而域名又分为三类(几个点就是几级域名)

1. 顶级域名  test.com
2. 二级域名  test.cn.com
3. 三级域名  test.mail.cn.com

例如: 当www.baidu.com访问的时候,客户端就会先给dns服务器发送信息, 目前我们的路由都集成了dns。 然后dns会先给根服务器发信息,根服务器返回.com的信息,然后dns 再去给.com发信息,.com返回ip地址给dns,同时dns会记录他的ip地址,最后返回给客户端ip地址,这就是相当于dns迭代查询

但是,实际上我们的dns都是会缓存的,dns 会先查询浏览器有没有缓存,然后再查询电脑有没有,然后去查询运营商有没有,这个就是dns的递归查询,当dns通过递归的方式去查询,没有找到的时候,才会去迭代查询向根服务器查询。

引用bilibili的一张图表示

截屏2021-12-05 下午3.56.37.png

性能优化点之 DNS减少解析次数

这里DNS查询也会有一定的时间,所以可以通过两点有关dns来进行优化

  • 减少DNS解析次数
    • 如果网站所有的资源内容都放在同一个域下面,这样的话访问整个网站就只会查找一次DNS, 但是,这样http在下载的时候就会出现资源排队的问题,会增加响应时间,所以,一般找一个折中的域名量
  • 进行DNS预获取, DNS Prefetch
    • DNS Prefetch 是尝试在请求资源之前解析域名,这可能是后面要加载的文件,也可能是用户尝试打开的链接目标。域名解析和内容载入时串行的网络操作,所以这个方式能减少用户的等待时间
    • HTML的link元素通过dns-prefetch的rel属性值提供此功能,然后再href属性中指要跨域的域名。多页面重复DNS预解析会增加DNS查询次数,并且DNS-prefetch仅对跨域的DNS查找有效

建立TCP链接

数据传输流程: 三次握手  -------->   数据传输   ---------->   四次挥手

先进行三次握手。三次握手实际上是一个 开辟资源,建立连接的过程。

第一次表明客户端可以发送消息,然后第二次表示服务端可以收到消息,也可以发出消息,第三次,表明客户端可以收到消息

  • 客户端建立连接,发送syn包到服务器,等待服务器确认

  • 服务器确认后,回传一个s y n + ack 表示自己收到

  • 客户端收到后,再次发送一个ac k ,表示握手成功,开始进行传送数据

服务器每次处理完一个请求,就关闭了连接,下次再次传输数据的时候,又需要重新进行建立TCP 链接,所以耗费了性能

性能优化点之 减少握手次数

http 1.1默认是持久连接。当客户端第一次请求资源后,并不会断开连接,而是服务器返回消息后,客户端继续发送消息。如果没有要发送的之后,客户端最后发送Connection: close 首部给服务器,这样就会关闭服务器,减少了每一次重新进行tcp连接

客户端和服务端传送数据

客户端发起http 请求

客户端向服务器端发起一个请求,进行资源的请求,如果资源特别大,然后距离也特别远的话,请求响应的时间就会变长。

HTTP优化之资源的合并与压缩

1. 减少请求次数,减少请求体积

在将资源打包的时候,通过webpack 进行资源的压缩与合并,利用一些插件优化代码体积,拆分资源。比如tree-sharking, 按需加载等,或者开启http 压缩(在request headers 中加入 accept-encodig: gzip)减小下载资源的体积。

一般我们会对图片进行优化,减少其体积,先来介绍下各个图片格式的优缺点

JPG  使用于较大的背景图,轮播图或者banner图。既可以保证图片质量,也不会有太大的体积,
     但是他的缺点就是如果图片颜色对比强烈的时候,就会导致图片模糊明显,而且不支持透明度处理。

Png  质量高,但是体积大,体积大, 所以主要是呈现一些小logo,或者颜色简单对比强烈的图片或者背景图

Svg  体积小,图片放大不失真

然后,在项目中,一些小图标,都使用css Sprites 来进行一次性请求,这样就节省了请求次数, 也可以将图片进行base64编码,然后直接写到src 中,浏览器会自动解析,但是如果图片进行base64编码后,图片体积会变为源文件的4/3,所以如果大图也编码的话,会变得非常大,即使少发http请求,但是文件变大,得不偿失。所以base64编码只适合一些特别小的图片

2. 减少单次请求所花费的时间,提高访问速度

用cdn来存放静态资源,需要注意的是,cdn 的服务器域名和服务器域名不相同,这样的话,请求图片或者css文件就不会带上cookie。

服务端处理http请求

服务端处理请求返回响应

上面说到,为了减少响应时间,提高访问速度,我们进行了代码体积等方面的优化,还有一个重要的优化其实就是利用浏览器的缓存机制。

http优化之 缓存机制

我们先来看下浏览器缓存机制,很多时候,我们喜欢把http缓存机制叫做浏览器缓存机制,其实两者还是有一些区别的,浏览器的缓存主要有下面这个几种,其中他包含http缓存。

  • Memory Cache
  • Service Worker cache
  • http cache
  • Push cache (HTTP 2 新增的)

然后我们详细看下http缓存机制

http缓存机制

Http 缓存分为强缓存和协商缓存,当强缓存没有命中的时候,才会走协商缓存。

强缓存

在http1.0 的时候,强缓存利用http头中的expries来控制,如果请求发出,服务器返回响应,会在response Headers 中将过期时间写入一个时间过期时间戳,后面再发送请求的时候,浏览器会根据expries 时间戳和本地时间进行判断, 判断目标有没有命中强缓存,如果命中,则直接从缓存中获取资源,返回http状态码为200,不会再与服务端通信,但是,本地时间可能会被修改,也可能服务器时间与本地时间设置不一致,所以, Http 1.1 中新增了 Cache-Control 字段 来代替expires, cache-control: max-age = 124435450, 通过max-age 来设置一个时间长度,在这个时间段内,缓存都有效,cache-Control 和expries 同时出现,要以cache-control 为准。 除了max-age 外。还有s-maxage, 如果s-maxage 和max-age 同时出现,则先考虑s-maxage, 如果s-maxage没有过期,就去向代理服务器请求缓存内容,但是是只针对将资源设置为pubic 的缓存。

Cache-control: max-age=1200, s-maxage=340000

// 注意:如果资源设置为public, 则资源既可以被浏览器缓存,也可以被代理服务器缓存,
        如果设置为private, 则资源只能被浏览器缓存。
禁止缓存

如果我们不想走缓存,还可以设置他的其他两个值 no-cache, no-store

如果设置 Cache-control: no-cache 后,则每一次请求都不会询问浏览器缓存,不会走强缓存,而是直接向服务端确认资源是否过期,直接去走协商缓存

如果设置 Cache-control: no-store 。则是不会使用任何缓存策略,直接向服务器发送请求,下载响应资源。

协商缓存(判断缓存还有没有效)

浏览器去向服务器确认缓存相关信息,来判断缓存是不是还有效,用来判断重新发起请求,还是读取本地缓存的资源。如果服务器提示资源没有改动,则资源会被重定向到浏览器缓存中,网络返回304状态码。

如果开启协商缓存,在第一次发起请求的时候,response header 会返回一个 last-Modified:fri, 1 mon 2021 12:44:52 GMT, 然后后面每次请求,请求头都会带上一个叫 if-Modified-Since: fri, 1 mon 2021 12:44:52 GMT 字段,值就是last-Modified 返回的值, 然后服务端接受到后,对比该时间和服务端资源返回的时间是不是一致,如果没有修改,则返回304响应, 如果修改了,则 response-header 中添加新的last-modified时间。

但是last-modified 中,如果资源保存后,但是并没有修改任何东西,这时候,last-modified也会再次改变,资源再次请求的时候,会被当作新资源,还有就是他只能检查到以秒为单位,没办法检查到毫秒时间的修改,所以,如果在毫秒时间修改了文件,last-modified并不会更新。

为了解决这两个问题,出现了Etag, Etag 是服务器为每个资源生成了一个资源唯一标识字符串,文件内容不同,etag的值就不同,第一次请求的时候,response-headers 返回etag的值,再次请求的时候,请求头都会带上一个 if-None-Match, 值为etag的返回值,然后服务端去进行对比,如果Etag 和 last-modified 同时存在,以etag为准。但是etag 生成需要服务器的额外开销。这是他的缺点。

缓存优化之 浏览器本地存储

除此以外,我们还有浏览器的本地存储,可以把一些资源存放到浏览器中。

cookie

http 是一个无状态协议,当服务器接受客户端请求返回响应的时候,并不会记录当前客户端是谁,结束后,下一次接收到客户端的请求并不知道当前客户端与刚刚是否相同,所以诞生了cookie.

cookie 大小只有4kb, 通过响应头里面的set-cookie 来指定要存的cookie,domain 为设置cookie页面的主机名, 同一个域名下所有请求都会携带cookie

set-cookie: name=lalala;domain=juejin.cn

Local Storage

存储大小为5M(5M代表字符串长度或者说10M 字节数),一般用他存储一些base64字符串,或者js,c ss,等不经常更新的资源

session Storage

只适用于当前会话,关闭后就会自动清除,刷新不会

INdexDB

新增的非关系型数据库 一般来说不会小于200M

渲染页面构建dom

浏览器拿到html文件后,开始解析html,然后就去请求遇到的js。css等文件,然后开启渲染,渲染我们分为客户端渲染和服务端渲染

客户端渲染 服务器把静态文件发送给客户端,客户端加载后,在浏览器运行一遍js,然后根据结果返回生成对应dom, 页面上的内容在html源文件里找不到,客户端渲染除了要加载html,还要等渲染所需的js加载完后,在运行一遍js,才能真正展示出来,所以容易出现白屏,解决的办法就是使用服务端渲染

服务端渲染 直接展示一个完整的网页,而且服务端渲染还可以进行搜素引擎快速展示,seo优化。

如果要渲染整个网页,需要浏览器内核里面的几大部分来配合

  1. HTML解析器: 将html文档经过词法分析输出,然后生成dom树
  2. Css 解析器: 解析css文档,生成样式规则,创建cssdom树
  3. 图层布局计算模块: 布局计算每个对象的精确位置和大小
  4. 视图绘制模块: 进行具体节点的图像绘制,将像素渲染到屏幕上
  5. javascript 引擎:编译执行javascript 代码

每一个页面渲染都经历了这几个阶段

  • 解析html, 在解析过程中发出了页面渲染所需要的各种外部资源
  • 计算样式,识别加载所有的css样式与dom树结合,生成render树(cssdom解析过程与dom解析过程是并行的)
  • 计算图层布局。计算元素的位置大小
  • 绘制图层 根据dom 代码结果,给每一个图层转为像素
  • 合并图层 合并图层,通过GPU绘制到屏幕上

新元素加入dom树的时候,css 就会查css 样式表,找到符合的样式应用到元素上,然后重新绘制dom, 所以我们可以优化查找css样式表的速度和dom绘制速度

css优化

css 规则都是从右边到左边匹配,所以少用标签选择器,减少嵌套层级

我们刚刚提到, cssdom和dom树合并成render tree, 默认情况下css是阻塞资源的,dom 解析完成,cssdom未完成,渲染就需要等着cssdom完成,所以,尽早将css下载,比如将css 放到header里,或者启用cdn实现静态资源加载速度优化。

页面渲染优化

因为js引擎是独立于渲染引擎的,js在文档哪里插入,就在哪里执行,当html解析器遇到script 时候,就会暂停渲染过程,将控制权交给js引擎。js对内联js代码直接执行,对外部js需要先获取脚本然后执行,等js执行完毕,再将控制权交给渲染引擎,来进行cssdom和dom的构建。因为浏览器不知道js会对页面做什么改变,所以加载js会阻塞其他执行。

所以我们可以控制js加载时机进行优化

js 加载三种方式。

  • 正常加载。 会阻塞cssdom和dom 构建

  • async模式。 不会阻塞浏览器做其他事情,js加载是异步的,加载完后立即执行。

  • defer模式。 不会阻塞浏览器,也是异步加载js, 但是js执行被推迟。只有等文档解析完成后,才会开始执行js文件。

性能优化之 减少操作dom。 减少重绘与回流,利用dom Fragment

js 操作修改dom本质上是修改了renderTree 变换导致。而重绘和重排主要是修改dom的时候触发。先简单说下重绘和回流

回流 当对dom 修改宽,高,或者隐藏元素的时候,浏览器就需要重新计算元素的属性,然后再将绘制图层。这个过程就是回流。也叫重排,重排一定会引起重绘

重绘 当对dom修改其样式如背景颜色,没有修改几何样式,不需要计算位置,直接绘制新的样式,就是重绘。

那么我们该如何避免呢?

如果频繁改动一个元素,需要进行多次计算。 这样会触发多次回流,那么我们可以将dom 设置为display:none., 然后计算完成后再进行display: block,这样只触发一次回流,尽量使用class.

在页面渲染的时候,我们js是无法进行交互的,这里又引出了js的事件循环机制,如何提高页面的可交互性,

事件循环

因为js 是单线程的(因为涉及到do m操作,多线程需要进行同步判断),但是遇到定时逻辑,网络请求等异步操作又回阻塞线程,所以,当代码执行的是,遇到同步任务直接执行,遇到异步任务就会根据任务类型分别放到宏任务队列和微任务队列,同步任务执行完后,查看微任务队列,如果有微任务,则保证把所有的微任务全部执行完(包括执行微任务中产生的新的微任务), 然后去执行查看宏任务队列,执行下一个宏任务,如果在执行宏队列的时候,产生了新的微任务,则执行完微任务再去接着执行宏队列 ,执行完后,查看微任务队列,重复循环,直到宏任务队列为空。

宏任务: setTimeout. setInterval, ajax , fetch,

微任务: promise.then() mutationObserver. Object.observe

// TODO: 持续更新中~

关闭TCP连接

当数据完成后,进行tcp断开链接

// TODO:

四次挥手

  • 客户端向服务端发起一个fin 报文
  • 服务端收到fin后,会发给客户端一个fin + ack 的报文
  • 如果服务器也想断开链接,给客户端发一个fin
  • 客户端收到fin 后,给服务器发一个ack, 进入time_wait然后过一段时间确保服务器收到ack后,才关闭

服务端收到ack后直接关闭,如果给服务端的ack丢失了,服务端会重新发fin + ack, 当客户端再次收到ack后,就知道之前ack丢失了,再重新发送ack

看到这里了,点个赞吧~~

如有错误,还请指出,共同进步~~