浏览器输入url到页面渲染全过程及性能优化

772 阅读20分钟

       一道经典的问题,却是包含了大部分前端知识体系,今天就由我所学所见总结出一些相对应的知识点。

主要知识框架包括:主要包括浏览器相关知识,输入URL后的过程,性能优化三个方面。

      审题一下注意到浏览器输入url,所以你应该先了解下浏览器的相关知识。

浏览器的相关知识

1.进程和线程

       进程:资源(内存)分配和携带的最小单元,可以包含多个线程。

       线程:cpu调度的最小单位,不分配资源,同个进程里的线程共享进程的资源。

2.浏览器包含哪些进程

浏览器是多进程的,而不是单进程,这样可以在其他进程死锁的情况不会影响其他进程正常运行。

       Browser进程:主要功能是控制页面级浏览器进程和进程间的交互

       GPU进程:用于3D绘制等 配合GUI线程渲染页面

        插件进程:每个浏览器插件都会有一个进程,如可观测的优化插件lighthouse等

        页面浏览器渲染进程:打开的每个tab都会创建一个,也是本文中的一代目男主。

3.页面浏览器渲染进程

      浏览器渲染进程包含很多线程,具体有下面几个重要的。

      GUI渲染线程:页面渲染此线程发挥作用,比如在readerTree渲染时,每次宏任务结束也会触发一次。

       JS引擎线程:浏览器中著名的js单线程,本文的二代目男主,运行时与GUI渲染线程互斥,event loop中的男主。

       事件触发线程:event loop的男二出现,定时器和http线程处理事件结束后 该线程会把它们放到事件队列里。

       定时器线程:处理定时器事件,结束时会把回调放到事件队列里。

       异步http请求线程:处理http请求事件,结束时会把回调放到事件队列里。

审题一下注意到浏览器输入url,那么我们已经了解浏览器,接下来就了解输入url后浏览器做了什么。

输入url的过程

        具体知识点包括1.DNS域名解析成IP,2.建立TCP连接,3.http报文相关,4.缓存策略,5.页面解析渲染,6.JS单线程与event loop。主要这6个方面吧,其中每个知识点里面都会有很多相关的知识点,那么下面让我们一层层揭开输入url过程的面纱。

1.DNS域名解析

        大致流程:

  • 如果浏览器有缓存,直接使用浏览器缓存,否则使用本机缓存,再没有的话就是用host

  • 如果本地没有,就向dns域名服务器查询(当然,中间可能还会经过路由,也有缓存等),查询到对应的IP

       dns解析会影响白屏时间,但前端代码层面对这方面优化无能为力。

2.建立TCP连接

       先介绍下五层网络模型

  1. 应用层 http请求
  2. 传输层 tcp 数据包
  3. 网络层 IP寻址 数据包
  4. 链路层 封装成帧
  5. 物理层 利用物理介质传输比特流0/1

      主要知识点在三次握手和四次挥手,具体也不多说了,大家肯定都有了解,直接贴一些老铁的好文地址:juejin.cn/post/684490…

3.http报文相关

    这边知识点会介绍1.get post的区别2.跨域3.报文结构分析4.http和https协议

1.get post的区别

  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
  • GET参数通过URL传递,POST放在Request body中(http约定)

  • GET请求在URL中传送的参数是有长度限制的(浏览器限制2K),而POST么有。

mp.weixin.qq.com/s?__biz=MzI…

2.跨域

   关于跨域之前我也总结了比较常用的一些解决方法

 juejin.cn/post/684490…

3.报文结构分析

报文一般包括了:通用头部请求/响应头部请求/响应体

通用头部

这也是开发人员见过的最多的信息,包括如下:

Request Url: 请求的web服务器地址

Request Method: 请求方式(Get、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE)

Status Code: 请求的返回状态码,如200代表成功

Remote Address: 请求的远程服务器地址(会转为IP)

其中最常用的请求方法我们在上面对post和get也做了一些讲解,下面重点看下常见的状态码和区间代表的意思,状态码能帮助快速定位问题。

200——表明该请求被成功地完成,所请求的资源发送回客户端
304——自从上次请求后,请求的网页未修改过,请客户端使用本地缓存
400——客户端请求有错(譬如可以是安全模块拦截)
401——请求未经授权
403——禁止访问(譬如可以是未登录时禁止)
404——资源未找到
500——服务器内部错误
503——服务不可用
...

1xx——指示信息,表示请求已接收,继续处理
2xx——成功,表示请求已被成功接收、理解、接受
3xx——重定向,要完成请求必须进行更进一步的操作
4xx——客户端错误,请求有语法错误或请求无法实现
5xx——服务器端错误,服务器未能实现合法的请求

请求/响应头部

请求和响应头部也是分析时常用到的

常用的请求头部(部分):

Accept: 接收类型,表示浏览器支持的MIME类型(对标服务端返回的Content-Type)
Accept-Encoding:浏览器支持的压缩类型,如gzip等,超出类型不能接收
Content-Type:客户端发送出去实体内容的类型
Cache-Control: 指定请求和响应遵循的缓存机制,如no-cache
If-Modified-Since:对应服务端的Last-Modified,用来匹配看文件是否变动,只能精确到1s之内,http1.0中
Expires:缓存控制,在这个时间内不会请求,直接使用缓存,http1.0,而且是服务端时间
Max-age:代表资源在本地缓存多少秒,有效时间内不会请求,而是使用缓存,http1.1中
If-None-Match:对应服务端的ETag,用来匹配文件内容是否改变(非常精确),http1.1中
Cookie: 有cookie并且同域访问时会自动带上
Connection: 当浏览器与服务器通信时对于长连接如何进行处理,如keep-alive
Host:请求的服务器URL
Origin:最初的请求是从哪里发起的(只会精确到端口),Origin比Referer更尊重隐私
Referer:该页面的来源URL(适用于所有类型的请求,会精确到详细页面地址,csrf拦截常用到这个字段)
User-Agent:用户客户端的一些必要信息,如UA头部等

常用的响应头部(部分)

Access-Control-Allow-Headers: 服务器端允许的请求Headers
Access-Control-Allow-Methods: 服务器端允许的请求方法
Access-Control-Allow-Origin: 服务器端允许的请求Origin头部(譬如为*)
Content-Type:服务端返回的实体内容的类型
Date:数据从服务器发送的时间
Cache-Control:告诉浏览器或其他客户,什么环境可以安全的缓存文档
Last-Modified:请求资源的最后修改时间
Expires:应该在什么时候认为文档已经过期,从而不再缓存它
Max-age:客户端的本地资源应该缓存多少秒,开启了Cache-Control后有效
ETag:请求变量的实体标签的当前值
Set-Cookie:设置和页面关联的cookie,服务器通过这个头部把cookie传给客户端
Keep-Alive:如果客户端有keep-alive,服务端也会有响应(如timeout=38)
Server:服务器的一些相关信息

一般来说,请求头部和响应头部是匹配分析的。

譬如,请求头部的Accept要和响应头部的Content-Type匹配,否则会报错

譬如,跨域请求时,请求头部的Origin要匹配响应头部的Access-Control-Allow-Origin,否则会报跨域错误

譬如,在使用缓存时,请求头部的If-Modified-SinceIf-None-Match分别和响应头部的Last-ModifiedETag对应

请求/响应体

一般我们在发起http请求时如果是get方法会放到params里面传输也体现在URL后面?连接的参数,post请求会放到body体内

响应报文的响应体通常是后端在接受到请求返回给前端的数据包,可能是一个HTML页面也有可能是一串json数据等

4.http和https协议

首先说下http的特点

http 1.0

默认使用的是短连接,也就是说,浏览器没进行一次http操作,就建立一次连接,任务结束就中断连接,譬如每一个静态资源请求时都是一个单独的连接

请求方法只有GET、POST、HEAD

Expires

If-Modified-Since/Last-Modified

http 1.1

默认使用长连接,使用长连接会有这一行Connection: keep-alive,在长连接的情况下,当一个网页打开完成后,客户端和服务端之间用于传输http的tcp连接不会关闭,如果客户端再次访问这个服务器的页面,会继续使用这一条已经建立的连接

请求方法只有GET、POST、HEAD、OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法

Cache-Control

If-None-Match/E-tag

http 2.0

是http的下一代规范

1.多路复用(即一个tcp/ip连接可以请求多个资源)

2.首部压缩(http头部压缩,减少体积)

3.二进制分帧(在应用层跟传送层之间增加了一个二进制分帧层,改进传输性能,实现低延迟和高吞吐量)

4.服务器端推送(服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端)

5.请求优先级(如果流被赋予了优先级,它就会基于这个优先级来处理,由服务器决定需要多少资源来处理该请求。)

https

https就是安全版本的http,譬如一些支付等操作基本都是基于https的,因为http请求的安全系数太低了。

https与http的区别就是: 在请求前,会建立ssl链接,确保接下来的通信都是加密的,无法被轻易截取分析

一般来说,如果要将网站升级成https,需要后端支持(后端需要申请证书等),然后https的开销也比http要大(因为需要额外建立安全链接以及加密等),所以一般来说http2.0配合https的体验更佳(因为http2.0更快了)

一般来说,主要关注的就是SSL/TLS的握手流程,如下(简述):

1. 浏览器请求建立SSL链接,并向服务端发送一个随机数–Client random和客户端支持的加密方法,比如RSA加密,此时是明文传输。 

2. 服务端从中选出一组加密算法与Hash算法,回复一个随机数–Server random,并将自己的身份信息以证书的形式发回给浏览器
(证书里包含了网站地址,非对称加密的公钥,以及证书颁发机构等信息)

3. 浏览器收到服务端的证书后
    
    - 验证证书的合法性(颁发机构是否合法,证书中包含的网址是否和正在访问的一样),如果证书信任,则浏览器会显示一个小锁头,否则会有提示
    
    - 用户接收证书后(不管信不信任),浏览会生产新的随机数–Premaster secret,然后证书中的公钥以及指定的加密方法加密`Premaster secret`,发送给服务器。
    
    - 利用Client random、Server random和Premaster secret通过一定的算法生成HTTP链接数据传输的对称加密key-`session key`
    
    - 使用约定好的HASH算法计算握手消息,并使用生成的`session key`对消息进行加密,最后将之前生成的所有信息发送给服务端。 
    
4. 服务端收到浏览器的回复

    - 利用已知的加解密方式与自己的私钥进行解密,获取`Premaster secret`
    
    - 和浏览器相同规则生成`session key`
    
    - 使用`session key`解密浏览器发来的握手消息,并验证Hash是否与浏览器发来的一致
    
    - 使用`session key`加密一段握手消息,发送给浏览器
    
5. 浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,

之后所有的https通信数据将由之前浏览器生成的session key并利用对称加密算法进行加密

4.缓存策略

很多时候,大家倾向于将浏览器缓存简单地理解为“HTTP 缓存”,通常存放在磁盘里“from disk cache”。但事实上,浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:

  1. Memory Cache
  2. Service Worker Cache
  3. HTTP Cache
  4. Push Cache

其中,“from memory cache”对标到 Memory Cache 类型,表示存在内存中,因为资源比较宝贵,常用来放一些base64图片等。“from ServiceWorker”对标到 Service Worker Cache 类型,必须以 https 协议为前提,会存放一些图片静态资源。至于 Push Cache,这个比较特殊,是 HTTP2 的新特性。

HTTP Cache,缓存可以简单的划分成两种类型:强缓存协商缓存

区别简述如下:

  • 强缓存(200 from cache)时,浏览器如果判断本地缓存未过期,就直接使用,无需发起http请求

  • 协商缓存(304)时,浏览器会向服务端发起http请求,然后服务端告诉浏览器文件未改变,让浏览器使用本地缓存

对于协商缓存,使用Ctrl + F5强制刷新可以使得缓存无效

强缓存的实现:从 expires 到 cache-control

http 1.0时 过去我们一直用 expires 

当服务器返回响应时,在 Response Headers 中将过期时间写入 expires 字段。像这样:

expires: Wed, 11 Sep 2019 16:12:18 GMT

可以看到,expires 是一个时间戳,接下来如果我们试图再次向服务器请求资源,浏览器就会先对比本地时间和 expires 的时间戳,如果本地时间小于 expires 设定的过期时间,那么就直接去缓存中取这个资源。

从这样的描述中大家也不难猜测,expires 是有问题的,它最大的问题在于对“本地时间”的依赖。如果服务端和客户端的时间设置可能不同,或者我直接手动去把客户端的时间改掉,那么 expires 将无法达到我们的预期。

考虑到 expires 的局限性,HTTP1.1 新增了 Cache-Control 字段来完成 expires 的任务。
expires 能做的事情,Cache-Control 都能做;expires 完成不了的事情,Cache-Control 也能做。因此,Cache-Control 可以视作是 expires 的完全替代方案。在当下的前端实践里,我们继续使用 expires 的唯一目的就是向下兼容

cache-control: max-age=31536000

如大家所见,在 Cache-Control 中,我们通过 max-age 来控制资源的有效期。max-age 不是一个时间戳,而是一个时间长度。在本例中,max-age 是 31536000 秒,它意味着该资源在 31536000 秒以内都是有效的,完美地规避了时间戳带来的潜在问题。

Cache-Control 相对于 expires 更加准确,它的优先级也更高。当 Cache-Control 与 expires 同时出现时,我们以 Cache-Control 为准。

no-cache和no-store

no-cache 绕开了浏览器:我们为资源设置了 no-cache 后,每一次发起请求都不会再去询问浏览器的缓存情况,而是直接向服务端去确认该资源是否过期(即走我们下文即将讲解的协商缓存的路线)。

no-store 比较绝情,顾名思义就是不使用任何缓存策略。在 no-cache 的基础上,它连服务端的缓存确认也绕开了,只允许你直接向服务端发送请求、并下载完整的响应。

协商缓存:浏览器与服务器合作之下的缓存策略

协商缓存依赖于服务端与浏览器之间的通信。

协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。

如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304

协商缓存的实现:从 Last-Modified 到 Etag

Last-Modified 是一个时间戳,如果我们启用了协商缓存,它会在首次请求时随着 Response Headers 返回:

Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT

随后我们每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:

If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT

服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;否则,返回如上图的 304 响应,Response Headers 不会再添加 Last-Modified 字段。

使用 Last-Modified 存在一些弊端,如果文件内容未更改或者更改再复原,也会导致整个文件的时间戳出现误判,服务器并没有正确感知文件的变化。为了解决这样的问题,Etag 作为 Last-Modified 的补充出现了。

Etag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的,反之亦然。因此 Etag 能够精准地感知文件的变化。

Etag 和 Last-Modified 类似,当首次请求时,我们会在响应头里获取到一个最初的标识符字符串,举个🌰,它可以是这样的:

ETag: W/"2a3b-1602480f459"

那么下一次请求时,请求头里就会带上一个值相同的、名为 if-None-Match 的字符串供服务端比对了:

If-None-Match: W/"2a3b-1602480f459"

Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能,这是它的弊端。因此启用 Etag 需要我们审时度势。正如我们刚刚所提到的——Etag 并不能替代 Last-Modified,它只能作为 Last-Modified 的补充和强化存在。 Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。

大概就是如果命中强缓存就直接从磁盘读出内容,Cache-Control优先级高,http code:200,如果没有强缓存会去检查弱缓存,Etag优先级较高http code:304。


5.页面解析渲染

    http请求拿到响应体html文本之后就到了页面解析,大体流程在五个步骤

1. 解析HTML,构建DOM树
2. 解析CSS,生成CSS规则树
3. 合并DOM树和CSS规则,生成render树
4. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
5. 绘制render树(paint/Repaint),绘制页面像素信息

  • DOM 树:解析 HTML 以创建的是 DOM 树(DOM tree ):渲染引擎开始解析 HTML 文档,转换树中的标签到 DOM 节点,它被称为“内容树”。

  • CSSOM 树:解析 CSS(包括外部 CSS 文件和样式元素)创建的是 CSSOM 树。CSSOM 的解析过程与 DOM 的解析过程是并行的

  • 渲染树:CSSOM 与 DOM 结合,之后我们得到的就是渲染树(Render tree )。

  • 布局渲染树:从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标,我们便得到了基于渲染树的布局渲染树(Layout of the render tree)。

  • 绘制渲染树: 遍历渲染树,每个节点将使用 UI 后端层来绘制。整个过程叫做绘制渲染树(Painting the render tree)。

这里Layout和Repaint的概念是有区别的:

  • Layout,也称为Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树

  • Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了

回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流, 所以优化方案中一般都包括,尽量避免回流。

什么会引起回流?

1.页面渲染初始化

2.DOM结构改变,比如删除了某个节点

3.render树变化,比如减少了padding

4.窗口resize

5.最复杂的一种:获取某些属性,引发回流,
很多浏览器会对回流做优化,会等到数量足够时做一次批处理回流,
但是除了render树的直接变化,当获取一些属性时,浏览器为了获得正确的值也会触发回流,这样使得浏览器优化无效,包括
    (1)offset(Top/Left/Width/Height)
     (2) scroll(Top/Left/Width/Height)
     (3) cilent(Top/Left/Width/Height)
     (4) width,height
     (5) 调用了getComputedStyle()或者IE的currentStyle

回流一定伴随着重绘,重绘却可以单独出现

所以一般会有一些优化方案,如:

  • 减少逐项更改样式,最好一次性更改style,或者将样式定义为class并一次性更新

  • 避免循环操作dom,创建一个documentFragment或div,在它上面应用所有DOM操作,最后再把它添加到window.document

  • 避免多次读取offset等属性。无法避免则将它们缓存到变量

  • 将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高


6.JS单线程与event loop

 通常js文件都在body的最下面的script标签内,当运行到js的时候,会创建一个全局执行上下文并在执行栈底,之前我也总结过执行上下文的相关知识

juejin.cn/post/684490…

然后当我们遇到异步事件也会有特定的处理规律也就是event loop,之前我也总结过event loop知识

juejin.cn/post/684490…

性能优化

关于性能优化我也总结了一篇文章,从webpack打包和页面渲染两个方面入手,对减小http请求数据包大小和减少http请求数量两个方面着手实践

juejin.cn/post/684490…