从输入URL到页面加载发生了什么
基于网上的大量参考资料,梳理对这一加载流程的理解。深知自己的认知理解有限,请大家多多拍砖。
这一过程的浏览器行为表象( 步骤分解 ):
- 输入URL并回车 ( 通过DNS解析具体IP地址 )
- 浏览器发起请求 ( 浏览器通过TCP发送HTTP请求 )
- 服务器返回数据 ( 服务器处理请求并返回HTTP报文 )
- 浏览器渲染页面
输入URL并回车
浏览器主线程解析URL、通过服务器地址查找具体IP地址( DNS解析 )
DNS的解析过程:
- 第一步:浏览器将会检查缓存中有没有这个域名对应解析过的IP地址,如果有,该解析过程将会结束。浏览器缓存是有限制的(Chrome对每个域名会默认缓存60s)
- 第二步:如果用户的浏览器中缓存中没有,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。(开发测试环境配置hosts的原因)
- 第三步:如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。
- 第四步:如果hosts与本地DNS解析器缓存都没有相应的映射关系,此时会访问递归DNS服务器(区域 - 运营商 - 根服务),然后递归DNS服务器会递归查找域名记录,返回结果
注:
- 起初互联网信息中心是整体管理一份hosts文件,由于网络规模不断扩大不得不产生一个可以有效管理主机名和ip地址之间对应关系的系统(NDS系统) - DNS就是互联网中的分布式数据库
- 我们根据dns解析原理去优化dns加载速度 预加载DNS:
<link rel="dns-prefetch" href="/img.alicdn.com">
浏览器怎么发起请求?
浏览器是通过HTTP发起的请求,而HTTP属于TCP/IP协议族的子级,TCP/IP协议族里最重要的一点就是分层(五层)
- 应用层:解析成IP地址并发送HTTP请求( NDS和HTTP属于应用层 )
- 传输层:三次握手建立TCP连接( 为应用层提供 - 靠谱的分片数据传输 )
- 网络层:IP寻址( 为传输层提供传输路线 )( 在与服务器之间通过多台计算机和网络设备进行传输时,网络层起的作用就是在众多的选项内选择一条传输路线 )
- 链路层:网卡
- 物理层:物理传输 ( 硬件范畴比特流、双绞线、电磁波 .. )
什么是TCP?
TCP负责建立连接、发送数据以及断开连接。TCP提供将应用层发来的数据顺利发送至服务端的可靠传输,为了实现这一目的,需要在应用层数据前端附加一个头TCP首部( 源端口号和目标端口号、序号、校验 ),之后传递给IP层,IP层负责提供传输路线
web开发者必须熟悉http。也就是前后端的http交互 ( http报文结构、缓存、跨域、cookie、安全...) 。 http是依托于TCP的,因此...
TCP如何实现的可靠传输?( 三次握手 )
TCP协议采用了三次握手策略( 确保双方都可以收到 )。TCP链接是全双工的,双方的数据读写可以通过一个连接进行。
形象一点的描述:
// 客户端(毛蛋):二狗二狗,我是毛蛋,我是毛蛋,听到请回答,听到请回答!
// 服务端(二狗):二狗收到,二狗收到,毛蛋请讲,毛蛋请讲,完毕!
// 客户端(毛蛋):毛蛋收到, ...
复制代码
四次挥手
四次挥手:保证双发数据都发送完毕,都认为可以断开。如果客户端发送关闭请求时,服务端没有需要传递給客户端的信息时,此时可以合并为一条数据发送会客户端。
// 客户端(第一次挥手):主动发起关闭请求(FIN报文段),客户端此时进入FIN_WAIT_1状态
// 服务端(第二次挥手):回复客户端(设置ACK报文段),服务端进入CLOSE_WAIT状态,客户端收到ACK报文后,进入FIN_WAIT_2状态
// 服务端(第三次挥手):服务器此时会观察自己是否还有数据没有发送給客户端,有,先发送数据再发送FIN报文,没有,则直接发送FIN报文給客户端,同时服务端进入LAST_ACK状态
// 客户端(第四次挥手):客户端收到服务端的FIN报文段,向服务器发送ACK报文,然后客户端进入TIME_WAIT状态,服务端收到客户端的ACK报文段后,就关闭连接;此时,客户端等待2MSL后依然没有收到服务端的回复,则证明服务端已正常关闭,客户端也可以关闭这个连接了
复制代码
注:
- 客户端最后为什么需要等待态( TIME_WAIT )?传递的 FIN 报文段有可能会丢失
- 什么时候进行四次挥手:根据
Connection
请求头,如果是Keep-alive
,服务器就保持TCP
链接,如果没有Keep-alive
或者主动 close ,则服务器 Response 传输完成后主动关闭TCP
链接 ( http1.1 默认开启 Keep-alive 的,在浏览器tab关闭时,TCP
链接才关闭 )
服务器返回数据
浏览器通过(HTTP-TCP-IP-链路-物理),到达服务器,服务器在通过协议进行一步步(层链路-IP-TCP-HTTP)反向解析,拿到HTTP信息后 - 服务端进行处理( 反向代理、安全拦截、跨域验证、业务逻辑... ),等待程序执行完毕后,再经过层层封装发送给前端,完成交互
各种HTTP头
通用头部
具有代表性的14个状态码
- 200:该请求的被成功处理,请求的资源送回客户端
- 204:该请求的被成功处理,但响应的报文信息不含主体内容( 客户端想服务端发送消息,而不需要返回新信息内容时使用 )
- 206:范围请求(Content-Range)
- 301:永久重定向(京东老域名 www.360buy.com)
- 302:临时重定向( 短链接 )
- 304:缓存( 下面有详细说明 )
- 307:临时重定向(类似302) - 更加严谨(不会从POST变成GET)
- 400:客户端请求有误( 请求报文存在错误语法,修改后重新发送 )
- 401:请求未经授权
- 403:禁止访问( 未获得访问权限、访问权限出现问题... )
- 404:资源未找到
- 500:服务器内部错误( 执行时发生错误、Bug、某些临时故障... )
- 503:服务器不可用( 服务器处于超负载或停机维护状态 )
常用的请求头和响应头
缓存
两种类型:强缓存(200 from cache)与协商缓存(304)
- 强缓存http1.1(Cache-Control/Max-Age)、http1.0(Pragma/Expires)
- 协商缓存http1.1(If-None-Match/E-tag)、http1.0(If-Modified-Since/Last-Modified) http1.1的方法总是优与http1.0,主流的方案缓存这四种方案总是会都加上 更好的方案是消灭304,除了主页之外都是强缓存,需要更新时修改静态资源的MD5戳即可 http资源缓存
浏览器渲染页面
浏览器拿到HTML文档,
- DOM解析(标记化和树构建) - DOM树
- CSSS解析 - CSSOM树
- 将DOM树和CSSOM树合并删除渲染树(Render Tree) - 其实可以把根html看做是一个层提升(就像整体的javascript本身就是一个宏任务一样) - Paint
- 层叠上下文处理 - 形成Layer Tree:把渲染树中的一些节点生成单独的一层( 层提升 ) - Paint
- 层合并处理 - 形成Graphics Lyaer(GPU硬件加速) - GPU直接绘制
- 附1:HTML无法用常规的自上而下或者自下而上的进行解析,因为浏览器历来对一些无效的HTML用法采用包容态度,解析过程需要不断循环往复。源内容在解析过程中通常不会改变,但是在结构中JS往往会添加额外的标记(重排 || 回流)。
- 附2:标记化:词法分析(起始标记、结束标记、属性名、属性值)。构建树:标记生成后,传递給构件树,标记一个构建一个,如此反复
- 附3:CSS解析与DOM解析同步进行(DOM解析结构,CSS解析样式,两者并不冲突)
- 附4:默认DOM和CSS加载与JS互斥(JS中可能会访问我们CSS中的样式 && 结构)
- 附5:生成Graphics Lyaer的元素(video、canvas、flash、CSS3D、perspective(透视)、opacity,transfrome应用了animation、transition时...)
说到渲染过程( 再说一说基本的优化手动 )
请求相关的优化手段
- 资源引入位置( 解析CSS会阻塞JS执行、解析JS会阻塞CSS解析和页面渲染,css放在头部,js放在底部 )
- 异步script标签
不可避免的在头部或者body中间加载JS文件, defer异步加载JS - 在DOM解析完后立即执行( 效果等同在body底部,主入口文件有一个即可 ),async异步加载JS - 在JS加载完后立即执行(建议独立的代码使用,如:baidu统计).
- 避免使用css@import
我的CSS代码中从来没有怎么写过,造成额外的请求,如果真的想用,使用sass,会自动合并(引入占位符不会造成代码重复)
- 注意空的scr
a标签空href,会重定向到当前页面地址 Form给空method,会提交表单到当前页面地址
- 我们根据dns解析原理去优化dns加载速度 页面加载优化:
<link rel="dns-prefetch" href="/img.alicdn.com">
DOM的相关的优化
- 缓存DOM
let CacheEl = {
container: document.querySelectorAll('.container'),// 只有querySelectorAll返回的NodeList为静态的
divCollection: document.getElementsByTagName('div),
// HTMLCollection为动态
$Box: $('#box')
};
复制代码
- 批量操作DOM
- 拼接完成后,再innerHTML更新DOM
- DocumentFragment对象实际上理论中能做到优化,但是
- 第二中方案比第一种方案时间要长,因为现代浏览器能优化的最终都会被浏览器优化
- DOM读写分离( 对性能影响很大 )
修改和访问DOM分开批量进行(读取DOM会触发浏览器一次渲染)
- 事件代理( 减少内存占用 )
其实DOM操作时代,最熟悉就是事件代理了(能动态捕获添加的节点)
- 最小化全局影响( 编程思维,减少内存 )
生命周期的概念:不用时清除定时器、不用时清除事件监听器、创建最小作用域(GC)、清除对象引用
资源加载优化
- 首屏加载之前,缩短白屏时间使用浏览器缓存( 图片、JS、CSS、字体、swf、音频、ajax GET请求 )
- ajax预加载场景( 短期内固定不变的数据:如 - 选项列表 )。GET请求可以被缓存。如果是POST请求可以配合localStorage做本地存储。先从本地去,如果取到了就直接用,因为页面在渲染的时候就已经请求过了。这样能达到节省时间的目的。
- 动态请求资源:动态创建Image对象,或者script对象,设置src,将节点append到页面(Image对象不需要),只要设置了src属性,浏览器就会发出请求。最简单的上报也是这么搞
- 空闲时间,为SPA的下一屏做预加载(在空闲时,悄悄的加载下一屏的内容)
- 预测用户操作,预先加载数据(如:亚马逊的二级菜单) - 技术要求较高
- 资源懒加载、图片懒加载、JS按需加载(业务逻辑比较复杂的系统,点击之后才触发对应的加载 webpack)、滚动性能提升:函数节流
总结:
- DNS解析
- TCP三次握手四次挥手
- 各种HTTP头信息
- 代表性的14种状态码
- 缓存
- 页面渲染流程
- 基本优化方案( webpack + 三大框架 默认优化 )
细碎点很多,有机会再继续 (⊙﹏⊙)b (⊙﹏⊙)b (⊙﹏⊙)b
- 跨域、Cookie、CSS(BFC)、class、JS预处理、执行上下文、作用域、this、ES6...
- web安全、https、http2.0 ...
- Event Loop(浏览器、Node)、Promise
附:这篇博客 也许 想表达 恩,也许就是一篇博客 (⊙﹏⊙)b