多角度理解浏览器工作(从输入url到页面呈现)

1,116 阅读30分钟

总结此篇的初衷有几点:

  1. 因为在工作上总是涉及到体验的问题,因此想了解浏览器怎么工作,从而可以更高维度去了解前端。

  2. 写远比读有成就感。外在发发疯,好在内在被憋疯。

  3. 整体梳理知识,把js、网络、渲染、浏览器安全等知识串起来,有个整体全新的认识。

  4. 输出自己好记忆风格的文章

  5. 每天坚持一点,增强自我效能感

写这篇文章的顺序是:先整个阅读有个整体框架,再细节上深究,再形成整体逻辑。

最近在看武志红的心理学课,有一句很经典的话,【成为真实的自己】,此文是站着巨人肩膀上输出的原创,展现自己所获所感。

通过此文章你将了解到:

  • 进程VS线程
  • 浏览器工作原理(HTTP请求到获取数据发生什么)
  • 浏览器渲染机制
  • JS运行机制解析(为什么是单线程、执行栈、任务队列、事件循环)
  • 前端优化方式

附加:

  • 任务队列和时间循环
  • 三次握手和四次挥手

看自己很早的笔记,就是复制几句很简单的字,现在静下心来花几天时间总结梳理下。另外想说的是,专栏没有花时间去排版出美美的样式,除了时间原因,还有因为样式太美会插入太多的标签, 不方便自己修改。

看完肯定是希望大家有所收获,如果值得收藏的话可以点赞或关注,支持一下

进程VS线程

多线程可以并行处理任务, 线程是不能单独存在的,它是由进程来启动和管理的。那什么又是进程呢?

一个进程就是一个程序的运行实例。
详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。

线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。

进程和线程之间的关系有以下 4 个特点:

  1. 线程是依附于进程的,进程中的任意一线程执行出错,都会导致整个进程的崩溃。

  2. 线程之间共享进程中的数据。

  3. 当一个进程关闭之后,操作系统会回收进程所占用的内存

  4. 进程之间的内容相互隔离

浏览器单进程:浏览器所有模块都运行在一个单进程中,包括网络、插件、JavaScript 运行环境、渲染引擎和页面

浏览器渲染机制

尽量详细分析渲染机制

构建DOM树

HTML内容转换为浏览器DOM树结构

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

转换过程:字节——》字符——》令牌——》节点——》对象模型(DOM)

盗图:有时间自己来画

分析具体转换含义:

  1. 字节转换成字符:浏览器从磁盘或者网络中读取HTML的原始字节流,并根据文件的指定编码(例如 UTF-8)将它们转换成各个字符流。

    以UTF-8为例,需要了解byte转化为UTF-8的转码规则。

  2. 字符转换成令牌:浏览器将字符串转换成 W3C HTML5 标准规定的各种令牌,也可以说通过词法分析器会被解释成Token(具体实现还需要进一步研究),比如:尖括号、标签名、字符串、字符串。

  3. 令牌转化成节点(词法分析):得到的令牌转换成定义其属性和规则的“对象”

  4. DOM树构建:HTML标记定义了不同标记之间的关系

在这个过程中,每一个环节都会调用对应的类去处理:

  • 词法分析: HTMLTokenizer 类
  • 令牌验证:XSSAuditor 类
  • 从令牌到节点: HTMLDocumentParser 类、 HTMLTreeBuilder 类
  • 从节点到 DOM 树: HTMLConstructionSite 类

DOM 树只能在渲染线程上创建和访问

DOM树是有很多子几点组成

下面附加几个定义,加深记忆。

字节:数据存储是以“字节”(Byte)为单位,数据传输大多是以“位”(bit,又名“比特”)为单位,一个位就代表一个0或1(即二进制),每8个位(bit,简写为b)组成一个字节(Byte,简写为B),是最小一级的信息单位。取值范围:0到255。

字符是指计算机中使用的文字和符号,比如1、2、3、A、B、C、~!·#¥%……—*()——+、等等。

ASCII码:一个英文字母(不分大小写)占一个字节的空间,一个中文汉字占两个字节的空间。一个二进制数字序列,在计算机中作为一个数字单元,一般为8位二进制数,换算为十进制。最小值-128,最大值127。如一个ASCII码就是一个字节。

UTF-8编码:一个英文字符等于一个字节,一个中文(含繁体)等于三个字节。中文标点占三个字节,英文标点占一个字节。

Unicode编码:一个英文等于两个字节,一个中文(含繁体)等于两个字节。中文标点占两个字节,英文标点占两个字节。

在 DOM 树构建完成后, 会触发 “DOMContentLoaded” 事件,当所有资源都被加载完成后,会触发 “onload” 事件。

这块看了不少,可是还是默认两可,希望更懂得的人能解释下。

样式计算

主要包含三个步骤:

  1. 格式化样式表:0,1字节流数据,浏览器无法直接去识别的,所以渲染引擎收到CSS文本数据后,转换为浏览器可以理解的结构-styleSheets
  2. 标准化样式表:样式计算之前需要标准化
body { font-size: 2em }     body { font-size: 32px }  
p {color:blue;}             p {color:rgba(255,0,0,0);}
span  {display: none}     
div {font-weight: bold}     div {font-weight: 700} 
  1. 计算每个DOM节点具体样式

计算规则:继承和层叠 继承:每个子节点会默认去继承父节点的样式,如果父节点中找不到,就会采用浏览器默认的样式,也叫UserAgent样式;

层叠:样式层叠,是CSS一个基本特征,它定义如何合并来自多个源的属性值的算法。某种意义上,它处于核心地位,具体的层叠规则属于深入 CSS 语言的范畴(不懂中???)。

在计算完样式之后,所有的样式值会被挂在到window.getComputedStyle当中,也就是可以通过JS来获取计算后的样式,非常方便

遵循了以上两种规则后,最终输出的内容是每个节点DOM的样式,被保存在ComputedStyle中

生成布局树

讲解完DOM树构建和样式计算后,接下来就是通过浏览器的布局系统确定元素位置,也就是生成一颗布局树(Layout Tree),之前说法叫 渲染树。

创建布局树:

  1. 遍历DOM中可见的节点;

  2. 忽略DOM树上不可见的元素(head元素,meta元素等,以及使用display:none属性的元素),它们都不会出现在布局树上。

盗图:有时间自己来画

创建布局树之后,要确定节点所在的坐标位置。不展开讲,说实话我看了之后一脸懵逼,有兴趣可以参考从Chrome源码看浏览器如何layout布局

中途总结:

  1. 构建DOM树:字节——》字符——》令牌——》节点——》DOM树(标记化、建树算法)
  2. 样式计算:格式化样式表、标准化样式表、以继承和层叠规则计算样式
  3. 生成布局树:创建布局树、计算节点坐标位置

在构建完布局树后,还需要进行一系列操作,比如是含有层叠上下文如何控制显示和隐藏等情况。还有一些复杂的场景,比如一些些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,还有比如是含有层叠上下文如何控制显示和隐藏等情况。

分层

  • 拥有层叠上下文属性的元素会被提升为单独一层
  • 需要裁剪(clip)的地方也会创建图层
  • 图层绘制

生成图层树(Layer Tree):览器的页面实际上被分成了很多图层,按照一定得顺序叠加在一起,这些图层叠加后合成了最终的页面

并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。

那什么情况下,渲染引擎会为特定的节点创建新图层呢?

有两种情况需要分别讨论,一种是显式合成,一种是隐式合成。

  1. 显式合成

一、 拥有层叠上下文的节点。

层叠上下文也基本上是有一些特定的CSS属性创建的,一般有以下情况:

  • HTML根元素本身就具有层叠上下文。
  • 普通元素设置position不为static并且设置了z-index属性,会产生层叠上下文。
  • 元素的 opacity 值不是 1
  • 元素的 transform 值不是 none
  • 元素的 filter 值不是 none
  • 元素的 isolation 值是isolate
  • will-change指定的属性值为上面任意一个。(will-change的作用后面会详细介绍)

二、需要剪裁(clip)的地方。

比如:设置固定高宽,文字过多出现进度条,这样进度条会被提升为一个新的图层

总结:元素有了层叠上下文的属性或者需要被剪裁,满足其中任意一点,就会被提升成为单独一层。

  1. 隐式合成

z-index比较低的节点会提升为一个单独的途图层,那么层叠等级比它高的节点都会成为一个独立的图层,这就是隐式新创建图层。

缺点: 根据【隐式合成】来说,在一个大型的项目中,一个z-index比较低的节点被提升为单独图层后,层叠在它上面的元素统统都会提升为单独的图层,我们知道,上千个图层,会增大内存的压力,有时候会让页面崩溃。 所以这也是写代码的一个优化点。

绘制

完成图层构建,接下来就是图层的绘制了。复杂的图层是有一块块组成的。按照顺序绘制。

绘制指令到不复杂,比如绘制粉色矩形或者黑色的线、坐标、样式。绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制

渲染进程包含多条线程,比如生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中

渲染流程图:

盗图:有时间自己来画

更新视图的方式

更新视图三种方式:

  1. 回流:对 DOM 结构的修改引发 DOM 几何尺寸变化的时候
  2. 重绘:当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等
  3. 合成:更改了一个既不要布局也不要绘制的属性,那么渲染引擎会跳过布局和绘制,直接执行后续的合成操作

举个例子:比如使用CSS的transform来实现动画效果,避免了回流跟重绘,直接在非主线程中执行合成动画操作。显然这样子的效率更高,毕竟这个是在非主线程上合成的,没有占用主线程资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。

利用这一点好处:

  • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
  • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
  • 对于 transform 和 opacity 效果,不会触发 layout 和 paint

GPU加速原因: 比如利用 CSS3 的transform、opacity、filter这些属性就可以实现合成的效果,也就是大家常说的GPU加速。

  • 在合成的情况下,直接跳过布局和绘制流程,进入非主线程处理部分,即直接交给合成线程处理。
  • 充分发挥GPU优势,合成线程生成位图的过程中会调用线程池,并在其中使用GPU进行加速生成,而GPU 是擅长处理位图数据的。
  • 没有占用主线程的资源,即使主线程卡住了,效果依然流畅展示。

单拎出来的附加内容

浏览器内核的相关知识

juejin.cn/post/684490…

  • GUI 渲染线程

负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。

当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。

执行时候:

  • JavaScript 引擎线程

负责解析JS脚本并运行相关代码,JS引擎一直等待着任务队列中任务的到来,然后进行处理;

一个Tab页面中无论什么时候都只有一个JS线程在运行,所以执行DOM渲染、script引入等操作都是同步的;

js引擎会通过 Event Loop 的机制,按顺序把任务放入栈中执行。

  • 事件触发线程

当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JavaScript 引擎的处理。

这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于 JavaScript 引擎是单线程的,所有这些事件都得排队等待 JavaScript 引擎处理。

  • 定时触发器线程

浏览器定时计数器并不是由 JavaScript 引擎计数的,这是因为 JavaScript 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确,所以通过单独线程来计时并触发定时是更为合理的方案。

常用的 setInterval 和 setTimeout 就在该线程中。

  • Http 异步请求线程

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

总结是评价自己对知识点的掌握和记忆程度,所以总结如下几点:

  1. 事件触发线程、HTTP异步请求线程最终都会走JavaScript 引擎线程,也就是加入任务处理队列中。也可以说最终一切事件都会加入到事件队列中
  2. 异步请求、定时器是回调函数加入事件队列中
  3. 浏览器内核主要线程总共包含GUI渲染线程、JS引擎线程、事件触发线程、定时触发器线程、异步请求线程
  4. js引擎是单线程中都是同步操作和步请求中的异步不一样,因为新开了一个异步请求线程,但是回调函数还是会回到JS引擎中等待处理

引发思考相关知识点:

  1. 任务队列包括宏任务队列(macro tasks)和微任务队列(micro tasks)。
  2. 事件循环(Event Loop)
  3. 密集型和高延迟任务,使用Web Workers
  4. 怎么解析HTML、CSS、构建DOM树和RenderObject 树、布局和绘制
  5. 是在栈中执行还是堆中?

**回顾下栈: **

  1. 栈是后进先出的一种数据结构,堆中存放在一些对象值;
  2. 栈存放着一些基础类型变量以及对象的指针;
  3. 当代码执行的时候,碰到函数,引擎会在栈里产生这个函数的执行栈,也叫执行上下文(理解:函数内又是另一个环境,也就是另一个执行上下文,想到作用查找,是再不断往上作用域查找);
  4. 执行栈(也叫执行上下文)的环境中存在着这个函数的私有作用域,上层作用域的指向,函数的参数,这个作用域中定义的变量以及这个作用域的this对象;
  5. 当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。
  6. 如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。。这个过程反复进行,直到执行栈中的代码全部执行完毕。

执行栈和任务队列?

执行栈也叫执行上下文;(点击函数、ajax请求函数、Promise函数都在栈中)

任务队列包括宏任务队列和微任务队列,是包含要执行的任务。

疑惑点:怎么样的才叫宏任务和微任务?分别有什么特点?查找相关资料得到:

宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering,requrestAnimationFrame,click、ajax 等回调方法

  1. 宏任务所处的队列就是宏任务队列
  2. 第一个宏任务队列中只有一个任务: 执行主线程的js代码
  3. 宏任务队列可以有多个
  4. 宏任务按顺序执行
  5. 当宏任务队列的中的任务全部执行完以后会查看是否有微任务队列,如果有先执行微任务队列中的所有任务,如果没有就查看是否有宏任务队列

微任务:process.nextTick(先Promise,再执行完所有nextTick(),再微任务。有些人说Promise是微任务,按照每次输出结果,我能说它是同步函数。), then(回调) ,Object.observer, MutationObserver

  1. 微任务所处的队列就是微任务队列
  2. 只有一个微任务队列
  3. 所有微任务也按顺序执行
  4. 如果有微任务队列,在以下场景会立即执行所有微任务
    • 在上一个宏任务队列执行完毕后
    • 每个回调之后且js执行栈中为空

进一步联想,NodeJs 的 Event Loop呢?

NodeJs 的 Event Loop 遵循的是 libuv

这个库是node作者自己写的,内部实现了一整套的异步io机制(内部使用c++和js实现),使我们开发异步程序变得简单,因为这个原因导致了一些js解析和浏览器的会不一样。

NodeJs 的运行是这样的:

  • 初始化 Event Loop
  • 执行您的主代码。这里同样,遇到异步处理,就会分配给对应的队列。直到主代码执行完毕。
  • 执行主代码中出现的所有微任务:先执行完所有nextTick(),然后在执行其它所有微任务。
  • 开始 Event Loop(这里就是进入宏任务)
  • 当每个阶段执行完毕后,都会执行所有微任务(先 nextTick,后其它),然后再进入下一个阶段。

最后我们来段代码彻底解析下两类任务队列在运行时的逻辑

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

176824359111012

以后面试问起事件循环:可以说先执行主程序,执行同步代码,再微任务,再执行宏任务中同步、微任务执行,按此顺序依次执行,直到最后。

注意: 浏览器在每个宏任务之间渲染页面

继续引发思考的另一个方面的知识:Web Workers

由于 JavaScript 引擎与 GUI 渲染线程是互斥的,如果 JavaScript 引擎执行了一些计算密集型或高延迟的任务,那么会导致 GUI 渲染线程被阻塞或拖慢。那么如何解决这个问题呢?嘿嘿,当然是使用本文的主角 —— Web Workers

Web Worker

  1. Web Worker中不能运行的代码:直接操纵 DOM 元素,或使用 window 对象中的某些方法和属性。

  2. Web Worker 中所支持的常用 APIs:

  • Cache:Cache 接口为缓存的 Request / Response 对象对提供存储机制,例如,作为- ServiceWorker 生命周期的一部分。
  • CustomEvent:用于创建自定义事件。
  • Fetch:Fetch API 提供了一个获取资源的接口(包括跨域请求)。任何使用过XMLHttpRequest 的人都能轻松上手,而且新的 API 提供了更强大和灵活的功能集。
  • Promise:Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。
  • FileReader:FileReader 对象允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。
  • IndexedDB:IndexedDB 是一种底层 API,用于客户端存储大量结构化数据,包括文件/二进制大型对象(blobs)。
  • WebSocket:WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。
  • XMLHttpRequest:XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。
  1. 主线程与 Web Workers 之间的通信

主线程和 Worker 线程相互之间使用 postMessage() 方法来发送信息,并且通过 onmessage 这个事件处理器来接收信息。数据的交互方式为传递副本,而不是直接共享数据。

  1. Web Workers 的分类
  • Web Worker 规范中定义了两类工作线程,分别是专用线程 Dedicated Worker 和共享线程 Shared Worker,
  • Dedicated Worker 只能为一个页面所使用,-
  • Shared Worker 则可以被多个页面所共享。

深入细节找时间写demo

DNS如何工作的

DNS协议是应用层协议,通常该协议运行在UDP协议之上,使用的是53端口号。

复制的: 客户端——》浏览器缓存——》本地hosts文件——》本地DNS解析器缓存——》本地DNS服务器——》其他域名服务器请求

我的理解: 本地hosts文件——》本地DNS缓存找映射关系——》路由器(有叫区域记录)——》根域名服务器——》子域名服务器——》返回到电脑服务器

从路由器——》根域名服务器——》子域名服务器,这个过程可能给会从你的域名注册商那获取,也可以从其他DNS服务商获取。

找到域名服务器后,就到达DNS解析服务。

isp:互联网服务提供商

网络通讯大部分是基于TCP/IP的,而TCP/IP是基于IP地址的。

  • DNS是计算机域名bai系统 (Domain Name System 或Domain Name Service) 的缩写,它是由域名解析器和域名服务器组成的
  • 域名服务器是指保存有该网络中所有主机的域名和对应IP地址,并具有将域名转换为IP地址功能的服务器。其中域名必须对应一个IP地址,一个域名可以有多个IP地址,而IP地址不一定有域名。域名系统采用类似目录树的等级结构。
  • 域名服务器为客户机/服务器模式中的服务器方,它主要有两种形式:主服务器和转发服务器。
  • 将域名映射为IP地址的过程就称为“域名解析”。
  • 本地是一个相对的概念,因为DNS服务是有很多级的,所以更靠近用户的那级服务器就叫做本地DNS服务器。比如114和8.8.8.8这样的DNS服务器处于根服务器之下所以可以算作是本地DNS服务,很多运营商都会在当地架设自己的DNS服务器储存着常用的域名映射,用来为用户提供更快的域名解析服务
  • DNS清楚缓存的方式和浏览器缓存不一样,浏览器的缓存,跟DNS缓存是分开的
  • 一般向本地DNS服务器发送请求是递归查询的,本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程。递归查询一般发送一次请求就够,迭代过程需要用户发送多次请求。

为什么是递归查询?最后研究

为什么DNS可以实现负载均衡? DNS可以在冗余的服务器上实现负载均衡,一般的大型网站使用多台服务器提供服务,因此一个域名可能会对应多个服务器地址。这样可以将用户的请求均衡的分配到各个不同的服务器上,这样来实现负载均衡。

当用户发起网站域名的 DNS 请求的时候,DNS 服务器返回这个域名所对应的服务器 IP 地址的集合 在每个回答中,会循环这些 IP 地址的顺序,用户一般会选择排在前面的地址发送请求。

根域名:单个句点(.)或句点用于末尾的名称
顶级域名:.com
二层域名:hy.com
子域:www.hy.com
主机名:h1.www.hy.com

CDN静态资源部署

定义:把静态资源文件和动态网页分集群部署,静态资源会被部署到CDN节点上,网页中引用的资源也会变成对应的部署路径

原理:不同地区的用户会访问到离自己最近的相同网络线路上的CDN节点,当请求达到CDN节点后,节点会判断自己的内容缓存是否有效,如果有效,则立即响应缓存内容给用户,从而加快响应速度。如果CDN节点的缓存失效,它会根据服务配置去我们的内容源服务器获取最新的资源响应给用户,并将内容缓存下来以便响应给后续访问的用户。因此,一个地区内只要有一个用户先加载资源,在CDN中建立了缓存,该地区的其他后续用户都能因此而受益。 不同地区的用户访问同一个域名却能得到不同CDN节点的IP地址,这要依赖于CDN服务商提供的智能域名解析服务,浏览器发起域名查询时,这种智能DNS服务会根据用户IP计算并返回离它最近的同网络CDN节点IP,引导浏览器与此节点建立连接以获取资源

HTTP请求流程

浏览器中的 HTTP 请求从发起到结束可以总结一共经历了如下八个阶段:

  • 构建请求
  • 查找缓存
  • 准备 IP 和端口
  • 等待 TCP 队列
  • 建立 TCP 连接
  • 发起 HTTP 请求
  • 服务器处理请求
  • 服务器返回请求和断开连接

当在url中输入www.baidu.com,会发生什么呢?下面一步步总结

1. 构建请求

首先会构建请求行,它包括:请求方法、请求url、协议版本

GET /index.html HTTP1.1

2. 查找缓存

在真正发起网络请求之前,浏览器会先在浏览器缓存中查询是否有要请求的文件浏览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术。 当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载。这样做的好处有(可以出面试题):

  • 缓解服务器端压力,提升性能(获取资源的耗时更短了);
  • 对于网站来说,缓存是实现快速资源加载的重要组成部分。

当然,如果缓存查找失败,就会进入网络请求过程了。

3. 准备 IP 和端口

在了解网络请求之前,先准备 IP 和端口,了解HTTP 和 TCP 的关系

  • HTTP 和 TCP 的关系

在HTTP工作之前,浏览器需要通过 TCP 与服务器建立连接。

HTTP 协议作为应用层协议,用来封装请求的文本信息;

TCP/IP 作传输层协议,将请求的文本信息发到网上;

也就是说 HTTP 的内容是通过 TCP 的传输数据阶段来实现的。

一个TCP连接生命周期:建立TCP连接、传输数据阶段、断开TCP连接。图如下

此图引发思考,想起知识点:

  1. HTTP 网络请求的第一步就是与服务器建立TCP连接;

  2. 发送HTTP请求:请求行、请求头、空行、请求数据;

  3. 服务器响应:响应码、响应头、响应体;

  4. 传输数据阶段:发送HTTP请求、服务器处理HTTP请求、服务器响应;

  5. 建立TCP连接经过三次握手,断开TCP连接需要经过四次挥手;

  6. 建立TCP连接的第一步? 第一步获取IP和端口信息,此时就想到DNS(域名服务系统,ip难记,所以有个域名和ip之间映射关系,它就是DNS),由此第一步就是浏览器会请求 DNS 返回域名对应的 IP(具体查看:DNS如何工作的),接下来是获取端口,通常情况下,如果 URL 没有特别指明端口号,那么 HTTP 协议默认是 80 端口。

  7. 断开TCP连接:四次挥手

4. 等待 TCP 队列

获取ip和端口,那么下一步是不是可以建立 TCP 连接了呢?

不一定的,这个得根据不同的浏览器来规定的,我们以Chrome浏览器为例,Chrome 有个机制,同一个域名同时最多只能建立 6 个 TCP 连接

如果在同一个域名下同时有 10 个请求发生,那么其中 4 个请求会进入排队等待状态,直至进行中的请求完成。

当然,如果当前请求数量少于 6,会直接进入下一步,建立 TCP 连接。

因此可能有个等待的状态

再引发思考:

  1. 如果好多张图片资源怎么办呢?
  2. HTTP1.1和HTTP1.0之间的区别

5. 建立 TCP 连接

排队等待结束后,终于可以建立TCP连接了。

再引发思考:

TCP和UDP的区别:

  • UDP在传输的过程中数据包容易丢失

  • 大文件被拆分为很多小的数据包时,小数据包会经不同的路由,不能同时到达接受端,UDP不知道如何组装这些数据包

  • UDP也不会有重发机制

  • TCP的出现,就是为了解决以上的问题,它是面向连接的,可靠的,基于字节流的传输层通行协议。

上面提到一个TCP连接生命周期:建立TCP连接、传输数据阶段、断开TCP连接。关于三次握手和四次挥手的内容,详见下面讲解。

6. 发送HTTP请求

中越可以发送HTTP请求了,不过HTTP中的数据就是在TCP连接的通讯过程中传输的。

发送的资源:

  • 请求行
  • 请求头
  • 请求体(只有post请求有,get请求没有)

7. 服务器端处理HTTP请求流程

  • 响应行:版本协议、状态码
  • 响应头
  • 响应体

常用状态码

着重介绍几种响应头

发送完响应头后,服务器就可以继续发送响应体的数据。

7. 断开连接

一般情况下,服务器发送完数据后,就要关闭TCP连接。不过有一种情况比较特殊,我们来看看 Connection:Keep-Alive

如果浏览器或者在服务器中加入其头信息如上面的字段的话,TCP连接会仍然保持,这样子浏览器就可以通过同一个TCP连接发送请求,保存TCP连接可以省下去下次请求需要建立连接的时间,提升资源加载速度。

比如,一个 Web 页面中内嵌的图片就都来自同一个 Web 站点,如果初始化了一个持久连接,你就可以复用该连接,以请求其他资源,而不需要重新再建立新的 TCP 连接。

三次握手和四次挥手

挥手多了一次是要多个回复

资源的预加载

DNS 预解析dns-prefetch

Head头部里面加入:
<link rel="dns-prefetch"href="//example.com">
# 请求这个域名下的文件时就不需要等待DNS查询了,也就是说在浏览器请求资源时,DNS查询就已经准备好了
# 该技术对使用第三方资源特别有用,比如jquery等

预连接 Preconnect

# 与 DNS 预解析类似,preconnect 不仅完成 DNS 预解析,同时还将进行 TCP 握手和建立传输层协议
<link rel="preconnect" href="http://example.com">

预获取 Prefetching

# 顾名思义,提前加载资源(未用到),首先要确定这个资源一定会在未来用到,然后提前加载,放入浏览器缓存中
<link rel="prefetch" href="image.png">

优先级Subresource

指定的预获取资源具有最高的优先级,在所有 prefetch 项之前进行
<link rel="subresource" href="styles.css">

预渲染 Prerender

# Prerender 预先加载的资源文件,也就是说可以让浏览器提前加载指定页面的所有资源
<link rel="prerender" href="http://example.com/index.html">

未来 Preload

# Preload 建议允许始终预加载某些资源,不像prefetch有可能被浏览器忽略,浏览器必须请求preload标记的资源
<link rel="preload" href="http://example.com/image.png">
# 存在兼容性

总结出优化方式(实践意义)

  1. 请求数量:图片转化成base64、精灵图、加入
  2. 缓存利用:添加 Expires 头或者 Cache-control 、ETags、减少 DNS 查询【怎么减少呢】、使 AJAX 缓存【怎么样缓存】
  3. 文件大小(请求带宽):压缩打包、去除不用的代码(移除重复的脚本)、按需引入、Gzip、用上eslint、将 JavaScript 和 CSS 独立成外部文件、图片压缩
  4. 图片太大:cdn加速、
  5. 减少回流:尽量使用absolute、visiblile、不获取标签位置、DOM结构的修改、scroll
  6. 动画:requestAnimateFrame
  7. 预加载、DNS解析、预渲染、预获取、预连接
  8. 对于 resize、scroll 等进行防抖/节流处理
  9. 动画使用transform或者opacity实现,不会触发 重绘 和 回流,在非主线程上合成的,没有占用主线程资源(好处原因:
  • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快;
  • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
  • 对于 transform 和 opacity 效果,不会触发 layout 和 paint)
  1. 使用createDocumentFragment进行批量的 DOM 操作
  2. 页面结构:将 CSS 样式放在页面的上方 (这样先请求出css,不会阻塞dom的进行,我在想是DOM树先形成还是样式技术)、避免使用 CSS 表达式、将JS脚本移动到底部(包括内联的)
  3. 避免重定向

收获

梳理了以上知识,也就明白了:CSS为什么要放到js前面,js要放在DOM后面,以及js的异步加载(async、defer),减少回流和重绘,动画使用transform或者opacity实现,减小文件大小等优化。

读万卷书,不如行万里路。

参考链接: juejin.cn/post/684790…

juejin.cn/post/685728…