从输入URL到页面呈现发生了什么

347 阅读10分钟

其实这个过程从浏览器层面看,涉及到两个主要进程,渲染进程和网络进程。大概过程就是网络进程负责请求资源,渲染进行负责解析渲染。还有浏览器负责统筹全局,管理网络进程和渲染进程之间的通信,以及浏览器的状态展示。

浏览器进程

获取正确的URL

首先浏览器进程会识别输入的是URL还是关键字,如果是关键字,会根据系统默认搜索引擎去拼接成一个带有关键字的完整URL。

如果是一个URL,会看这个URL是否完整,将URL补充为完整的URL,并进行URL解析,将ACSII码解析为正常字符。

页面变为加载状态并执行当前页面卸载事件

页面的加载状态变为加载中,如果当前页面设置了卸载事件,会去执行卸载事件,这个卸载事件一般用于如果有未完成的表单,让用户选择是否离开当前页面等。如果没有这个卸载事件或者用户允许继续的话,浏览器会将拼接好的URL交给网络进程。

网络进程

缓存查找

网络进程接收到这个URL首先会去进行本地缓存匹配。如果本地有对应资源,则进行强缓存匹配。

缓存是为了提高页面加载的速度,缩减请求时间和次数。

缓存的一般是JS,CSS,图片等等静态文件,如果被加载的资源需要缓存,头中会带有强缓存信息和协商缓存信息,强缓存使用cache-control来标识缓存的具体时间,以秒为单位,还有可能有expire字段,它是HTTP1.0版本的内容,使用格林威治时间标识资源过期,会有时间格式,本地修改时间不匹配等问题,比cache-control优先级低。

如果强缓存不匹配,则会进入协商缓存阶段。

时间对比: 服务端向客户端发送资源时携带的最后修改时间last-modify,客户端向服务端发送请求带上If-Modified-Since,不需要生成哈希去对比,性能比较好,但是无法识别秒级以内的修改。

标识对比: 服务端向客户端发送资源时携带的e-tag资源标识,客户端向服务端发送请求带上If-None-Match,需要生成哈希去对比,性能不如时间对比,但是可以实现精确缓存。

缓存一般存在本地的memory cache和disk cache上。

memory cache内存缓存,最快,但是存活时间最短,渲染进程结束,内存缓存就没了,比较大的CSS JS文件会被丢进磁盘,反之丢进内存。

disk cache磁盘缓存,比内存慢,但是容量大时长多,内存使用率高时,文件被丢进磁盘。

还有pushcache是做服务器推送的,service worker一般用于离线缓存。

DNS解析

协商缓存阶段会发起网络请求,前置条件就是DNS解析,这一步的目的是为了解析出IP地址和端口号,如果没有端口号,HTTP默认使用80,HTTPS默认使用443端口。 DNS解析会先从浏览器缓存开始查找,一直递归查找到本地DNS服务器(在城市的某个角落),如果还没找到,由本地DNS向权威DNS解析服务器进行迭代查找,找到结果递归返回给浏览器。

内网递归查找:递归查找的主要思想是:如果主机所询问的本地域名服务器不知道被查询的域名的IP地址,那么本地域名服务器就以DNS客户的身份去查找。

  1. 浏览器缓存
  2. 系统缓存
  3. Host表
  4. 路由器
  5. 本地非权威DNS解析服务器

这一步的优化思路就是缓存,可以使用缓存dns-prefetch。

本地非权威DNS解析服务器之后,也就是外网,又分为转发和迭代。 迭代查找:迭代查找的主要思想:当根域名服务器收到本地域名服务器发出的迭代查询请求报文时,要么给出所要查询的IP地址,要么告诉本地服务器:“你下一步应当向哪一个域名服务器进行查询”。 转发查找:如果用的是转发模式,此DNS服务器就会把请求转发至上一级DNS服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根DNS或把转请求转至上上级,以此循环。转发是一种特殊的递归。

  1. 根域名服务器 www.baidu.com 返回com的位置
  2. 顶级域名服务器 返回baidu.com的位置
  3. 权威域名服务器 返回最终IP

注意,这里的IP有可能是多个。

Dns请求虽然占用了很少的带宽,但会有很高的延迟,由其以移动网络会更加明显。通过dns预解析技术可以很好的降低延迟.DNS Prefetch 是一种DNS 预解析技术,当浏览网页时,浏览器会在加载网页时对网页中的域名进行解析缓存,这样在单击当前网页中的连接时就无需进行DNS的解析,减少用户等待时间,提高用户体验。 但是也要慎用,多页面重复DNS预解析会增加重复DNS查询次数。

<link rel="dns-prefetch" href="http://img.jb51.net" />

以及这里如果有CDN的话,还需要考虑到CDN,待补充....

建立连接

TCP连接

三次握手

客户端向服务端发起连接请求,携带随机码SYN,服务端收到请求后确认客户端发信能力正常,返回ACK=SYN+1,并携带自己的随机码SYN,客户端确定服务端收发能力正常,同样返回ACK=SYN+1.这一步是为了确定双方收发能力正常。

TSL连接

如果是HTTPS协议,还需要进行TSL连接。 交换问候 主要是为了交换随机数

  1. 客户端发送Client Hello消息,包含支持的协议版本,密码套件,会话ID,和一个随机数等
  2. 服务器收到后会回复Service Hello,包含选择的版本和密码套件,随机数。之后依次发送服务器签了名的证书,和公钥。
  3. 客户端会逐级进行证书验证。

秘钥交换

  1. 客户端按照密码套件的要求生成公钥,使用client key change发送给服务端。
  2. 现在客户端和服务端都有了密钥交换的参数。两方根据算法算出Pre-master参数,黑客拿不到pre-master就拿不到主密钥,会话也就安全了。之后的通信都改为加密通信,握手正式结束。

构建请求

构建请求头,请求行,和关联的cookies,向服务器发送请求。

数据包传输

  1. http请求加上TCP头部——包括源端口号、目的程序端口号和用于校验数据完整性的序号,向下传输
  2. 网络层在数据包上加上IP头部——包括源IP地址和目的IP地址,继续向下传输到底层
  3. 底层通过物理网络传输给目的服务器主机
  4. 目的服务器主机网络层接收到数据包,解析出IP头部,识别出数据部分,将解开的数据包向上传输到传输层
  5. 目的服务器主机传输层获取到数据包,解析出TCP头部,识别端口,将解开的数据包向上传输到应用层

服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并再逆向一圈发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。

浏览器状态更新,进程之间通信

网络进程将获取到的包进行解析,根据content-type决定处理方式,如果是数据流,会中断当前请求,开启一个下载进程。如果是text/html,则通知浏览器进程获取文档准备渲染,由浏览器进程开启一个渲染进程,如果新页面与之前页面属于同一个站点,则复用渲染进程。

浏览器通知渲染进程后,渲染进程开始和网络进程建立数据传输管道获取HTML,结束后会通知浏览器进程,停止加载状态,更新安全状态,抛弃旧页面。

渲染进程

解析HTML,开启预解析线程

这步输入的是HTML字符串,输出的是DOM树。

渲染主线程输入HTML字符串,将它解析成便于操作的DOM树。同时开启一个预解析线程,扫描文档,下载外部资源。

解析CSS 样式计算

这步的输入是样式表,输出的是带有样式的DOM树。 预解析线程遇到样式表文件,交由网络进程加载完后会进行解析为stylesheets对象,交还渲染主线程进行合并,样式计算阶段会根据样式的继承,优先级关系计算出 DOM 节点中每个元素的具体样式,最终输出是DOM节点样式,放在每个DOM节点的ComputedStyle属性里。主要过程:

  1. 把 CSS 转换为浏览器能够理解的结构样式表对象
  2. 转换样式表中的属性值,使其标准化
  3. 根据CSS继承规则和层叠规则计算出 DOM 树中每个节点的具体样式,默认使用浏览器自带的useragent样式表。

CSS由预解析线程加载,不会阻塞HTML解析,但是会阻塞HTML渲染,因为JS有操作CSS的权限,DOM的渲染也需要样式计算,必须等样式表加载完才可以渲染。

布局阶段

布局阶段的输入是带有样式的DOM树,输出是layout树。该阶段主要是创建布局树和计算元素的可见属性,将不可见的元素删除,新增伪元素,计算元素的相对宽高等。

layout树有一个规则,行盒和快盒不能在一个层级上,字符串必须包在行盒里,所以该阶段会修修补补,增加匿名行盒和块盒。

分层

分层的输入是layout树,输出是分层树。

为什么要分层?每一层的修改频率是不同的,不分层会造成无关的渲染。

opacity,transom等会影响分层,will- change可以修改分层逻辑(确定是分层导致卡顿的时候再改)。

绘制

该阶段输出是分层树,输出是每一层的绘制指令。将指令交给合成线程。

分块

合成线程开启N个分块器线程,将每个层分成多个小块,结果返回合成线程。

光栅化

该阶段主要是将块转化为位图。优先渲染靠近视口的块。这个阶段会用到GPU加速,由GPU来完成。

GPU渲染

GUP对接系统,计算出每个位图在屏幕的位置,完成绘制。(为什么不直接对接系统?因为要保持沙箱隔离,减少污染)

解析JS

JS的解析必然阻塞渲染,但是加载可以交给预解析线程去加载。 这步可以使用defer和async属性去优化。 这两个属性的作用都是将JS文件交给预解析线程进行解析,区别是执行时机。defer是加载之后不执行,等待HTML加载解析结束,DOMCONTENTLOAD事件之前执行,可以保证执行顺序。 async是加载完之后立即执行,因为加载没办法保证顺序,所以执行顺序也不能保证。