访问⽹页流程
1. 浏览器发送请求到 dns服务器,dns服务器进⾏域名解析,解析完成之后浏览器拿到了⽹页服务器的IP地址
2. 浏览器根据拿回来的ip地址发送http请求给⽹页服务器
3. ⽹页服务器接收到http请求并处理,从⾃⼰的硬盘⽬录⾥到浏览器请求的⽹页⽂件并返回给浏览器
4. 浏览器接收到⽹页服务器返回的⽹页⽂件,开始⽤⾃⼰的内核渲染⽹页,并最终展⽰到显⽰器上
这⾥的每⼀步都环环相扣,中间哪个步骤都不能掉链⼦。并且每个步骤的执⾏速度,都会影响到我们对某个⽹站打开速度的直观感受。我们再把影响这四个步骤的速度的因素来逐⼀分析:
浏览器发送请求到 dns服务器,dns服务器进⾏域名解析,解析完成之后浏览器拿到了⽹页服务器的IP
地址
浏览器初步解析⽤户往地址栏输⼊的字符串。
如果⽤户输⼊的字符串是合法的⽹址:浏览器会检查⾃带的预加载HSTS列表(HTTP严格传输安全列表),
这个列表⾥包含了那些请求浏览器只使⽤ https 进⾏连接的⽹站。如果⽹站在这个列表⾥,浏览器会使⽤ https ⽽不是 http 协议,否则,没有标明协议的url域名会默认使⽤ http 协议访问。(p.s. ⼀个⽹站哪怕不在 HSTS 列表⾥,也可以要求浏览器对⾃⼰使⽤HSTS政策进⾏访问。浏览器向⽹站发出第⼀个HTTP请求之后,⽹站会返回浏览器⼀个响应,请求浏览器只使⽤HTTPS发送请求。然⽽,就是这第⼀个HTTP请求,却可能会使⽤户受到⿊客攻击,这也是为什么现代浏览器都预置了HSTS列表)
如果⽤户输⼊的字符串不是⽹址,只是瞎⼏把打的词语:浏览器会将地址栏中输⼊的⽂字传给⽤户设置的默认搜索引擎,⽐如百度。
⼤部分情况下,在把⽂字传递给搜索引擎的时候,URL会带有特定的⼀串字符,⽤来告诉搜索引擎这次搜索来⾃这个特定浏览器。这时⽤户想要访问的域名就是百度的域名加上在地址栏⾥瞎⼏把打的搜索关键字‘谁是世界第⼀帅’最终拼凑出来的了
在以上两种情况中,浏览器都会检查输⼊⽤户输⼊的字符串是否含有不是 a-z, A-Z,0-9之类的字符。如果⽤户输⼊的域名有⾮ASCII 字符的话,浏览器会对域名部分使⽤ punycode 编码。例如中国,会被浏览器⽤Punycode转换为:xn--fiqs8s. cn。punycode编码是因为操作系统的核⼼都是英⽂组成,早期的dns服务器只⽀持英⽂域名的解析,并且解析也是由英⽂代码交换,所以dns服务器并不⽀持直接的中⽂域名解析,所有中⽂域名的解析都需要先转成punycode码。其实⽬前所说和各种浏览器都完美⽀持
后来才出现的中⽂域名,只是浏览器给中⽂域名⾃动转码了,这样就不需要⽼旧的dns服务器再次安装中⽂域名转码控件来兼容中⽂域名的解析了。
浏览器这时候得到了⽤户想要访问的⽹页服务器的域名,下⼀步是要根据这个域名拿到⽹页服务器的ip地址。
浏览器会检查该域名是否在⾃⼰的缓存当中,因为浏览器默认会把⽤户访问过的⽹站缓存在本地。如果浏览器缓存中没有,浏览器就会去调⽤操作系统的getHostByName库函数进⾏查询。getHostByName函数会⾸先检查域名是否在本地的hosts⽂件⾥,host是⼀个记录着域名 -> ip地址映射关系表的⽂件,hosts的位置不同的操作系统有所不同。如果 getHostByName没有在本地的 hosts⽂件⾥到域名对应的ip地址,它将会向 dns服务器发送⼀条dns查询请求,去获取域名对应的IP地址。
dns服务器开始解析。dns服务器硬件配置差,机房温度太⾼,受到ddos攻击,不合理的解析协议算法等,都会导致dns服务器域名解析速度慢。
另:好的⽹络架构应该会在局域⽹内设⽴⼀个缓存服务器,把那些局域⽹内⼤家都经常请求的⽹址的域名ip映射,甚⾄是⽹址内容都缓存下来,就不⽤跑去⼤⽼远的dns服务器那⾥先解析地址,之后再取⽹页⽂件了。就好⽐是全世界⼈民都喜欢吃麦当劳的薯条,但麦当劳不会只在美国开唯⼀⼀家全球总店,⽽是世界各地都开了分店,⽽且每家分店都有薯条卖,这样⼤家就不⽤跑去美国总店吃薯条了,出家门右
转就能到分店吃薯条了,在⽹络世界⾥,这个技术叫做CDN。
浏览器根据拿回来的ip地址发送http/https请求给⽹页服务器
浏览器通过本机的操作系统的http模块发送http请求,和⽹页服务器通过经典的tcp三次握⼿之后建⽴可靠的连接(从硬件层⾯来说,⽤户的http请求数据流依次会经过⽤户的主机上的⽹卡 ->⽔晶头 -> ⽹线 ...),这⼀步的速度没啥好说的,主要就是 tcp 和 http 这些协议的算法的空间复杂度,时间复杂度的问题。
这个步骤可能会掉链⼦,举个掉链⼦的栗⼦:中间⼈攻击。⿊客截包之后篡改http请求头的 request host,让⽤户的请求最终发去了别的地⽅,⽽⾮⽤户的⽬标⽹页服务器。⽐如⽤户想去真·⽹页服务器请求⼀个修改密码的⽹页,结果这个请求发了去⿊客服务器那⾥,⿊客服务器给⽤户返回了⼀张⾼仿的修改密码页,⽤户不假思索就往这个⽹页上输⼊了⾃⼰的密码,然后就..........
真实案例:邮件重置密码时,劫持了邮件的内容,将host替换掉,然后⽤户点击发起链接的事后,⾝份认证信息(这⾥⼀般指的是⽹站传给重置密码者的随机token)会⾃动传到恶意的host上⾯,从⽽导致攻击者可以劫持账户(已知⾝份认证信息)。
http请求头就好⽐是包裹上⾯贴着的快递单,上⾯写着这个包裹从哪来,要去哪等等信息,被随意篡改会出事的。
⽹页服务器接收到http请求并处理,从⾃⼰的硬盘⽬录⾥到浏览器请求的⽹页⽂件并返回给浏览器
以上这步和⽹页服务器的性能有关。
⽹页服务器的http服务选择的是⽀持⾼并发的nginx,还是Apache,或者IIS都有影响,他们都属于http协议的实现,底层可能涉及到操作系统的io速度/线程的异步执⾏/内存管理等,我对此涉猎不深,略过不表。题外话:go有buffer ,python和node都没有buffer,后⾯俩加了buffer,io那项⾄少再快⼀半。所以服务端的http服务,我看好 go。
有个疑问:如果⽹页服务器的硬盘⽤的都是ssd,可能 io 速度能快⼀些,对打开⽤户访问某个⽹站的速度有直观上的提升吗?
浏览器接收到⽹页服务器返回的⽹页⽂件,开始⽤⾃⼰的内核渲染⽹页,并最终展⽰到显⽰器上
(这⼀步不考虑服务端渲染的情况,我们讨论最普通的⽤户端渲染)
浏览器开始进⾏html解析
浏览器内核有很多种,但都包含 js引擎, css引擎,html解析引擎。
html解析引擎的主要⼯作是⽣成dom节点树。dom树是以dom元素以及其属性为节点的树。
由于不能使⽤常⽤的解析技术,浏览器创造了专门⽤于解析HTML的解析器。解析算法在 HTML5 标准规范中有详细介绍,算法主要包含了两个阶段:标记化和 dom树的构建。
html不能使⽤常见的⾃顶向下或⾃底向上⽅法来进⾏解析,主要原因是以下⼏点① html本⾝可容错的特性② HTML本⾝可能是残缺的,对于常见的残缺,浏览器需要有传统的容错机制来⽀持它们,⽐如<input/>可能会写漏/写成了<input> ③解析过程需要反复。对于其他语⾔来说,源码不会在解析过程中发⽣变化,是静态的。但是对于HTML来说,例如js脚本中包含的 document.write() ⽅法会在源码中添加内容,也就是说,解析过程实际上会动态改变最初输⼊的html。
注意,解析 HTML ⽹页时永远不会出现“语法错误”,浏览器会⾃动修复所有错误,然后继续解析。
html解析完成
浏览器开始加载⽹页的外部资源(CSS,图像,Javascript ⽂件等)。这个说来话长,⽆数前端⼯程师为了能减少这⾥加载资源要发送的http请求数量,减⼩外部资源的体积,⽤了各种令⼈窒息的操作·····················⽐如把整个⽹站⽤到的⼩图标放在⼀张png的雪碧图,webpack的模块按需加载
此时浏览器把⽂档标记为“可交互的”,浏览器开始解析处于推迟(defer)模式的脚本,也就是那些需要在⽂档解析完毕之后再执⾏的脚本。之后⽂档的状态会变为‘完成’,浏览器会执⾏ onload 事件。
js引擎开始执⾏同步的 js 代码。
css引擎开始解析css代码,渲染页⾯(这个过程和传统⼯⼚⾥报纸的印刷⼗分类似,先把元素框的⼤概位置布局固定好,再往上细致地喷颜⾊纹理之类)
根据css语法和句法去分析.CSS⽂件和 <style> 标签包含的内容
每个CSS⽂件都被解析成⼀个样式表对象,这个对象⾥包含了带有选择器的CSS规则,和对应CSS语法的对象服务器地址
CSS解析器可能是⾃顶向下的,也可能是使⽤解析器⽣成器⽣成的⾃底向上的解析器
通过遍历DOM节点树创建⼀个“Frame 树”或“渲染树”,并计算每个节点的各个CSS样式值
通过累加⼦节点的宽度,该节点的⽔平内边距(padding)、边框(border)和外边距(margin),⾃底向上的计算 Frame树中每个节点⾸的选(preferred)宽度
通过⾃顶向下的给每个节点的⼦节点分配可⾏宽度,计算每个节点的实际宽度
通过应⽤⽂字折⾏、累加⼦节点的⾼度和此节点的内边距 (padding)、边框 (border) 和外边距 (margin),⾃底向上的计算每个节点的⾼度
使⽤上⾯的计算结果构建每个节点的(x,y)坐标
当存在元素使⽤ floated,位置有 absolutely 或 relatively 属性的时候,会有更多复杂的计算。
创建 layer (图层)来表⽰页⾯中的哪些部分可以成组的被绘制,⽽不⽤被重新栅格化处理(就是像素化,可以简单理解为计算屏幕上每个像素格⼦的颜⾊值)。每个帧对象都被分配给⼀个层。
页⾯上的每个层都被喷绘了纹理(?)
每个层的帧对象都会被遍历,计算机执⾏绘图命令绘制各个层,此过程可能由CPU执⾏栅格化处理(就是像素化,计算屏幕上每个像素格⼦的颜⾊值),或者直接通过 D2D / SkiaGL 在GPU上绘制
上⾯所有步骤都可能利⽤到最近⼀次页⾯渲染时计算出来的各个值,这样可以减少不少计算量
计算出各个层的最终位置,⼀组命令由 Direct3D / OpenGL发出,GPU命令缓冲区清空,命令传⾄GPU并异步渲染,帧被送到Window Server。
GPU开始把css引擎的计算结果渲染到⽤户的显⽰器上
在渲染过程中,图形处理层可能使⽤通⽤⽤途的 CPU,也可能使⽤图形处理器 GPU
当使⽤GPU⽤于图形渲染时,图形驱动软件会把任务分成多个部分,这样可以充分利⽤GPU 强⼤的并⾏计算能⼒,⽤于在渲染过程中进⾏⼤量的浮点计算。
后期渲染与⽤户引发的处理:渲染结束后,浏览器根据某些时间机制运⾏JavaScript代码( ⽐如Google Doodle动画 ) 或与⽤户交互 ( 在搜索栏输⼊关键字获得搜索建议 ) 。类似Flash 和 Java的插件也会运⾏,尽管Google主页⾥没有。这些脚本可以触发⽹络请求,也可能改变⽹页的内容和布局,产⽣⼜⼀轮渲染与绘制(重排和重绘)