浏览器工作原理
第一部分 导航
1.DNS域名解析(应用层实现)
注意,如果配置过CDN,DNS解析过程会发生变化,第三步不再是查找权威域名服务器,而是智能DNS
2.传输层三次握手建立TCP连接
3.TLS协商
对于HTTPS建立的安全连接,还需要额外进行一次TLS negotiate
- 客户端hello。浏览器向服务端发送一条消息,包含TLS版本号、加密套件和一串在浏览器随机生成的字节,简记为第1随机数或客户端随机数
- 服务端hello。服务端向浏览器发送一条消息,包含TLS版本号、加密套件和一串在服务端随机生成的字节,简记为第2随机数或服务端随机数
- 服务端发送SSL证书(一般是CA认证颁发、安装到服务器上的)
- 服务端交换公钥
- 服务端消息完毕
- 浏览器向CA核实认证服务器发送的SSL证书。之后在浏览器生成一串随机字节,简记为第三随机数或预主密钥,并将公钥加密后的预主密钥发给服务端
- 服务器用私钥解密得到预主密钥,根据客户端随机数、服务端随机数和预主密钥生成会话密钥;客户端同样生成会话密钥,存储在浏览器内存中
- 浏览器和服务器使用会话密钥进行对称加密传输数据
TLS会话密钥存储在哪里
内存中,而不是cache或者storage等地方
处于安全性和隐私性,会话相关的数据如会话密钥、会话ID等都在内存中,关闭标签页之后需要重新建立连接
preconnect dns-prefetch
浏览器在空闲时间预先进行:
dns-prefetch
: DNS lookuppreconnect
: DNS lookup + TCP handshake + TLS negotiation
第二部分 资源请求与加载
1.应用层发送HTTP请求
请求主要分为三部分:method headers body
method方法除了简单的get post之外还有遵从restful规范的put delete patch等方法,需要注意的是options,是发送preflight
预检请求的方法,如果一个HTTP Request不存从以下两点安全性要求:
安全的方法:GET POST HEAD
安全的headers:仅允许自定义下列 header:
- Accept
- Accept- Language
- Content-Language
- Content-Type 的值为 application/x-www-form-urlencoded,multipart/form-data 或 text/plain。
浏览器基于安全性考虑就会发送preflight(预检请求,方法不是POST/GET等,而是OPTIONS),通过之后才会发送真正的请求
2.HTTP响应
状态码、响应头、响应体
3.下载初始页面资源
现代应用往往采取前后端分离的模式、CSR渲染方式,因此初始资源往往是简单的html和大量css、js脚本以及静态资源
对于SSR,初始资源是已经装配可以直接呈现的html页面、少量css和js脚本、静态资源
4.浏览器的强缓存和协商缓存
强缓存
第一次成功发送请求并且成功获取响应之后,如果后台设置了强缓存,会强制浏览器将服务端提供的资源缓存在硬盘(disk cache)或者内存(memory cache)中。
下次刷新浏览器发送同样的请求,如果没有超出浏览器缓存的时间限制(没有过期),浏览器会直接返回请求内容,不会再通知服务端、请求服务端。
Expires
Expires指定过期时间,如果没有过期,直接从浏览器硬盘或内存中取出强缓存资源
HTTP1.0
Expires存在的问题
Expires指定过期时间,由服务端返回,却是和客户端时间对比,如果客户端和服务端时间不一致就会不准确
Cache-Control
max-age指定过期时长秒数,Cache-Control: max-age=3600
HTTP1.1
Cache-Contorl其他key-value :
对比
max-age的优先级高于expires,前者是HTTP1.1支持,后者HTTP1.0支持。
协商缓存
如果超出了max-age或expires规定的时间,服务器强缓存的资源就过期了,考虑协商缓存:
Last-Modified和If-Modified-Since
客户端每次请求都带上If-Modified-Since,当服务端对比资源的最后修改时间Last-Modified和If-Modified-Since
- 如果值相等,代表资源从该时间之后从未改变过,返回304状态码和空响应体,浏览器拿到后知道原本可能过期的强缓存内容还可以继续使用。
- 如果值不相等,说明资源改变了,就会返回200状态码,响应体内为最新资源
Last-Modified和If-Modified-Since的问题
以秒为单位更新,无法感知1s内发生的资源变化
ETag和If-None-Match
根据文件内容生成唯一标识,不再将时间作为评判资源是否改变的标准
对比
ETag和If-None-Match更精确、优先级更高:
Last-Modified是服务器文件的最后更改时间(只精确到秒),而ETag是服务器文件的唯一标识
- 某些文件修改非常频繁,比如在秒以下的时间内进行修改(比方说 1s 内修改了 N 次),If-Modified-Since能检查到的粒度时 s 级的,这种修改无法判断(或者说 UNIX 记录 MTIME只能精确到秒)
- 某些服务器不能精确得到的文件的最后修改时间
所以使用E-tag能更精确控制缓存
5.四次挥手断开TCP连接
第三部分 渲染
当浏览器的网络线程收到HTML文档后,会产生一个render任务,并将其传递给渲染主线程的消息队列。
在事件循环机制的作用下,渲染主线程取出消息队列中的render任务,开启渲染流程。
渲染流程分为多个阶段:
- 解析HTML和CSS
- 样式计算
- 布局
- 分层
- 绘制
- 分块
- 光栅化
- 画
1.解析 - Parse HTML、Parse Stylesheet
渲染的第一步是解析HTML
解析过程中遇到CSS下载并解析,遇JS下载并执行
为了提高解析效率,浏览器在开始解析前,会启动一个预解析线程
CSS不会阻塞HTML解析,JS会阻塞HTMl解析
如果主线程解析到link标签,此时外部CSS文件还没有下载好,主线程不会等待,继续解析后续的HTML。这是因为下载和解析CSS的任务在预解析线程中进行,这是CSS不会阻塞HTML解析的根本原因【区分于preload和prefetch】
如果主线程解析到script标签,会立刻停止解析HTML,转而等待下载JS并执行,之后才继续解析HTML。这是因为JS代码执行过程中可能会修改DOM树,所以必须暂停DOM树的生成,这也是JS的会阻塞HTML解析的根本原因【改进:async、defer】,预解析线程只能帮忙分担一点下载JS的任务
执行JS的V8引擎基于JIT(Justin Runtime)实现,边编译边执行
为什么不把js代码直接编译成机器码,而是有中间的字节码?
因为浏览器的运行环境可能不同,比如操作系统、CPU硬件资源可能不同,为了实现跨平台的兼容通用,JS代码统一解析成字节码,字节码的解释器和编译器适配各个平台,最终编译成机器码,由浏览器调度硬件资源执行指令
解析完成后,得到DOM树和CSSOM树,浏览器的默认样式、内联样式、外部样式、行内样式均会包含在CSSOM中
如何从控制台访问操作css style sheet
1
document.styleSheets
添加样式:
2.样式计算 - Recalculate Style
渲染主线程遍历DOM树,依次为每个DOM Node计算最终样式,即Computed Style
在这一过程中,计算样式值会变为解析样式值,比如
red
会变成rgb(255,0,0)
;em
会变成px
样式计算完成后,得到带有最终样式的DOM树
在 CSS 中有两个概念:
- 计算 (computed) 样式值是所有 CSS 规则和 CSS 继承都应用后的值,这是 CSS 级联(cascade)的结果。它看起来像
height:1em
或font-size:125%
。- 解析 (resolved) 样式值是最终应用于元素的样式值。诸如
1em
或125%
这样的值是相对的。浏览器将使用计算(computed)值,并使所有单位均为固定的,且为绝对单位,例如:height:20px
或font-size:16px
。对于几何属性,解析(resolved)值可能具有浮点,例如:width:50.5px
。很久以前,创建了
getComputedStyle
来获取计算(computed)值,但事实证明,解析(resolved)值要方便得多,标准也因此发生了变化。所以,现在
getComputedStyle
实际上返回的是属性的解析值(resolved)。
3.布局 - Layout
渲染主线程遍历DOM树,依次计算每个DOM节点的几何尺寸、位置信息,如节点的宽高,包含块的位置
大部分时候,DOM树和Layout树并非一一对应
比如
display:none
的节点没有几何信息,因此不会生成到布局树;
又比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。
布局完成后,得到Layout树
注意:布局阶段的div并不是DOM对象,而是C++内部的对象
4.分层 - Layer
渲染主线程会使用一套负责的策略对整个Layout分层
分层的好处:将来某一个层发生改变之后只需 要对该层局部更新,提升效率
滚动条、堆叠上下文、z-index、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过will-change
属性更大程度的影响分层结果(不要滥用)
5.绘制 - Paint
渲染主线程会为每一层生成如何绘制的指令
渲染主线程的工作到此为止,剩余步骤交给其他线程
6.分块 - Tiling
渲染主线程将每个图层的绘制信息提交给合成线程Compositor Thread
合成线程从线程池中拿多个线程,共同将每个图层分为若干小区域块
7.光栅化 - raster
合成线程将块信息交给GPU进程
GPU进程开启多个线程完成光栅化,光栅化是将每个块变成位图,优先处理靠近视口的块
canvas基于位图,绘制的单位是像素,依赖画布分辨率,图片放大会失真
svg基于矢量图,图形放缩不会影响显示效果
8.画 - draw
合成线程拿到每个层、每个块的位图后,生成一个个指引信息quad
指引会标示出每个位图应该画到屏幕的哪个位置,考虑旋转、放缩等变形
变形发生在合成线程,与渲染主线程无关,这是transform效率高的本质原因
合成线程会把quad提交给GPU进程,由GPU进程产生系统调用,提交给GPU硬件,最终绘制成为“帧”