浏览器工作原理

183 阅读20分钟

这是我的第一篇掘金博客,开启掘金写作之路。
@author: gmx

个人关于浏览器工作原理的总结,官方文档developer.mozilla.org/zh-CN/docs/…

渲染页面

1. 导航

导航是加载web页面的第一步,触发方式有:输入URL,点击链接,提交表单, etc。

主要分为三个步骤:(1) DNS查找;(2) TCP三次握手;(3) TLS协商

DNS查找

寻找页面资源的位置,导航到www.baidu.com ,HTML页面被定为到IP地址为220.181.38.251的服务器。如果以前没有访问过这个网站,则需要进行DNS查找。

基本概念

DNS:域名系统,进行域名和IP地址转换的服务器

域名:具有层次结构,从上到下为,根域名,顶级域名(com, net, org, cn, edu, ...etc),二级域名,三级域名,四级域名。www.baidu.com 从右至左,分别为com顶级域名,baidu二级域名,www三级域名

域名缓存:在域名服务器解析的时候,使用缓存保存域名和IP地址的映射。分为浏览器缓存和操作系统缓存。

浏览器缓存:浏览器在获取网站域名的实际IP地址后会对其进行缓存,减少网络请求的损耗

操作系统缓存:操作系统缓存其实是用户自己配置的hosts文件

本地域名服务器:一般为本地区的域名服务器,比如校园网的本地DNS在校园机房

查找方式

迭代查询:转发器按照域名级别高低,先后查询根服务器,.com域服务器,baidu.com域服务器,www.baidu.com 域服务器,最终得到授权的应答。需要经过多次挨个查询,才能得到结果。

递归查询:转发器将相应的查询结果返回至本地DNS服务器192.168.200.1,本地DNS服务器将查询结果返回至主机,最终得到www.baidu.com 的ns记录。因为本地DNS服务器不是www.baidu.com 的授权解析服务器,所以查询得出的结果是非权威应答。只需要发出一次请求就能得到相应的结果。比如:主机-本地DNS,本地DNS-转发器。

查询过程

  1. 搜索浏览器缓存,命中转6结束
  2. 搜索操作系统缓存,命中转6,结束
  3. 操作系统将域名发送至本地DNS,本地DNS采用递归查询自己的DNS缓存,命中转6结束
  4. 本地DNS向上级域名服务器进行迭代查询
  5. 首先本地DNS向根域名服务器发起请求,根域名服务器返回顶级域名服务器(.com)给本地服务器
    1. 本地拿到顶级域名服务器地址,向其发送请求获取权限域名服务器地址(.baidu.com)
    2. 本地DNS向权限域名服务器发起请求,最终得到www.baidu.com 对应的IP地址
  6. 操作系统将IP地址返回给浏览器,同时进行操作系统缓存IP地址
  7. 浏览器得到域名对应的IP地址,缓存IP地址与域名的映射,发起请求。

TCP握手

一旦获取到服务器的IP地址,发起请求,请求数据包进入协议栈,通过上层协议Https可以协商网络TCP套接字连接的一些参数;TCP/UDP负责数据收发;浏览器通过TCP三次握手与服务器建立连接。

TLS协商

为了在https上建立安全连接,必须进行TLS协商,它决定了用什么密码加密通信,验证服务器,在进行真实数据传输之前建立安全连接。同时,增加了加载页面的等待时间

响应

建立连接后,浏览器代表用户发送一个初始的http get 请求,通常为请求一个HTML文件。

初始请求的响应包含所接受数据的第一个字节,通常为14kb的数据。

TCP慢开始/14kb规则

慢开始是一种均衡网络连接速度的算法,慢开始逐渐增加发送数据的数量直到达到网络的最大带宽。逐渐建立适合网络能力的传输速度,避免拥塞

当服务器用TCP包来发送数据时,客户端通过返回确认帧来确认传输。由于硬件和网络条件,连接的容量是有限的,如果服务器发包太快,它们可能会被丢弃,意味着将不会有确认帧的返回,传输时延增加。

流量控制:流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止分组丢失的。流量控制根本目的是防止分组丢失,它是构成TCP可靠性的一方面。

实现:滑动窗口协议(连续ARQ协议)

拥塞控制:作用于网络,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况;常用的方法就是: (1) 慢开始、拥塞避免; (2) 快重传、快恢复。


解析

一旦浏览器收到数据的第一块,它就可以开始解析收到的信息。“推测性解析”,“解析”是浏览器将通过网络接收的数据转换为DOM和CSSOM的步骤,通过渲染器把DOM和CSSOM在屏幕上绘制成页面。

DOM是浏览器标记的内部表示。DOM也是被暴露的,可以通过JavaScript中的各种API进行DOM操作。

即使请求页面的HTML大于初始的14KB数据包,浏览器也将开始解析并尝试根据其拥有的数据进行渲染。这就是为什么在前14Kb中包含浏览器开始渲染页面所需的所有内容,或者至少包含页面模板(第一次渲染所需的CSS和HTML)对于web性能优化来说是重要的。但是在渲染到屏幕上面之前,HTML、CSS、JavaScript必须被解析完成。

构建DOM树

第一步是处理HTML标记并构造DOM树。HTML标记包括开始和结束标记,以及属性名和值。 如果文档格式良好,则解析它会简单而快速。解析器将标记化的输入解析到文档中,构建文档树。

DOM树描述了文档的内容。元素是第一个标签也是文档树的根节点。树反映了不同标记之间的关系和层次结构。嵌套在其他标记中的标记是子节点。DOM节点的数量越多,构建DOM树所需的时间就越长。如遇script,会阻塞HTML解析

预加载扫描器

解析可用的内容并请求高优先级资源,如CSS、JavaScript和web字体,将在后台检索资源,以便在主HTML解析器到达请求的资源时,它们可能已经在运行,或者已经被下载。预加载扫描仪提供的优化减少了阻塞。

当主线程在解析HTML和CSS时,预加载扫描器将找到脚本和图像,并开始下载它们。为了确保脚本不会阻塞进程,当JavaScript解析和执行顺序不重要时,可以添加async属性或defer属性。

构建CSSOM树

第二步是处理CSS并构建CSSOM树。浏览器将CSS规则转换为可以理解和使用的样式映射。浏览器遍历CSS中的每个规则集,根据CSS选择器创建具有父、子和兄弟关系的节点树。

CSSOM树包括来自用户代理样式表的样式。浏览器从适用于节点的最通用规则开始,并通过应用更具体的规则递归地优化计算的样式。换句话说,它级联属性值。

js脚本编译

当CSS被解析并创建CSSOM时,其他资源,包括JavaScript文件正在下载(多亏了preload scanner)。JavaScript被解释、编译、解析和执行。脚本被解析为抽象语法树。一些浏览器引擎使用”Abstract Syntax Tree“并将其传递到解释器中,输出在主线程上执行的字节码。

构建功能辅助树

浏览器还构建辅助设备用于分析和解释内容的辅助功能(accessibility )树。可访问性对象模型(AOM)类似于DOM的语义版本。当DOM更新时,浏览器会更新辅助功能树。辅助技术本身无法修改可访问性树。

渲染

渲染步骤包括样式、布局、绘制,在某些情况下还包括合成。在解析步骤中创建的CSSOM树和DOM树组合成一个Render树,然后用于计算每个可见元素的布局,然后将其绘制到屏幕上。在某些情况下,可以将内容提升到它们自己的层并进行合成,通过在GPU而不是CPU上绘制屏幕的一部分来提高性能,从而释放主线程。

Style

第三步是将DOM和CSSOM组合成一个Render树,计算样式树或渲染树从DOM树的根开始构建,遍历每个可见节点

Layout

第四步是在渲染树上运行布局以计算每个节点的几何体。布局是确定呈现树中所有节点的宽度、高度和位置,以及确定页面上每个对象的大小和位置的过程。回流是对页面的任何部分或整个文档的任何后续大小和位置的确定。

第一次确定节点的大小和位置称为布局。随后对节点大小和位置的重新计算称为回流。

Paint

最后一步是将各个节点绘制到屏幕上,第一次出现的节点称为first meaningful paint。在绘制或光栅化阶段,浏览器将在布局阶段计算的每个框转换为屏幕上的实际像素。绘画包括将元素的每个可视部分绘制到屏幕上,包括文本、颜色、边框、阴影和替换的元素.

绘制可以将布局树中的元素分解为多个层。将内容提升到GPU上的层(而不是CPU上的主线程)可以提高绘制和重新绘制性能。

Compositing

当文档的各个部分以不同的层绘制,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确显示内容。回流会触发重排和重绘

交互

一旦主线程绘制页面完成,你会认为我们已经“准备好了”,但事实并非如此。如果加载包含JavaScript(并且延迟到onload事件激发后执行),则主线程可能很忙,无法用于滚动、触摸和其他交互。

”Time to Interactive“(TTI)是测量从第一个请求导致DNS查找和SSL连接到页面可交互时所用的时间——可交互是”First Contentful Paint“之后的时间点,页面在50ms内响应用户的交互。如果主线程正在解析、编译和执行JavaScript,则它不可用,因此无法及时(小于50ms)响应用户交互。

如何避免重排重绘

重排:当渲染树的一部分必须更新并且节点的尺寸发生了变化,浏览器会使渲染树中收影响的部分失效,并重新构造渲染树

重绘:元素的外观发生变化所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观(颜色类)

重排必定重绘,但重绘不一定重排

引发重排

  1. 添加、删除可见的DOM
  2. 元素的位置/尺寸(margin, padding, border, height, width...etc)改变
  3. 页面渲染初始化
  4. 浏览器窗口改变
  5. 获取某些属性。当获取一些属性时,浏览器为取得正确的值也会触发重排(有队列,不重排,拿不到最新的值),导致队列刷新。属性包括:offsetTop, offsetLeft, offsetHeight, offsetWidth, scrollTop, scrollLeft, scrollWidth, scrollHeight, clientTop, clientLeft, clientWidth, clientHeight, getComputedStyle()(currentStyle in IE)

重排优化

  1. 浏览器固有的优化。 维护一个重排队列,将会引起重排的操作放入队列,定量或定时flush队列,进行一批处理,将多次重排转变为一次重排。
  2. 不要一条一条修改DOM的样式。 以添加类删除类的方式修改DOM样式
  3. 不要把DOM节点的属性值,放在一个循环里当成循环变量。避免循环一次改一次
  4. 为动画的HTML标签使用fixed或absolute定位,脱离文档流,以免频繁更改位置引起重排
  5. 尽量减少table布局。一个小的改动可能会造成整个table的重新布局。(table及其内部元素除外,它可能需要多次计算才能确定其在渲染树中的位置,花3倍于同等元素的时间)
  6. 不要在布局信息改变的时候做查询(会导致队列强制刷新, offsetTop之类的查询)

存储

cookie, localStorage, sessionStorage三者的异同点

  1. 共同点:都是保存在浏览器端,且受同源策略的限制。
  2. 生存期:cookie 有时效限制,localStorage不主动删除一直存在本地浏览器,sessionStorage关闭标签页,数据自动删除。
  3. 大小:cookie不超过4k,本地存储则要大得多(5M左右)。
  4. 使用:cookie在发起请求时,自动发送给服务器,和session配合实现跟踪浏览器用户身份认证。localStorage和sessionStorage不会主动发送,主要用于存储数据如toekn
  5. 安全性:WebStorage不会随着HTTP header发送到服务器端,所以安全性相对于cookie来说比较高一些,不会担心截获。
  6. 方便性:WebStorage提供了一些方法,数据操作比cookie方便。支持事件监听机制,可以将数据更新的通知发送给监听者。
  7. 作用域:sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面; localStorage和cookie是在所有同源窗口中共享的

会过期的localStorage

  1. 惰性删除

某个键值对过期之后,不会马上被删除,而是在下次被使用时,被检查到过期,才能删除。

var lsc = (function(self) {
  var prefix = 'one_more_lsc_'
  self.set = function (key, val, expires) {
    key = prefix + key
    val = JSON.stringify({ 'val': val, 'expires': new Date().getTime() + expires * 1000 })
    localStorage.setItem(key, val)
  }
  self.get = function(key) {
    key = prefix + key
    var val = localStorage.getItem(key)
    if (!val) return null
    val = JSON.parse(val)
    if (val.expires < new Date().getTime()) {
      localStorage.removeItem(key)
      return null
    }
    return val.val
  }
  return self
}(lsc || {}))
lsc.set('token', 'Bearxxx', 1)
setTimeout(function(){
  console.log(lsc.get('token'))
}, 2000)
      console.log(lsc.get('token'))

明显缺点:一直不用key,一直不删除,占存储空间

  1. 定时删除

每隔一段时间执行一次删除操作,通过限制删除操作执行的次数和频率,来减少删除操作对CPU的长期占用;另一方面,定时删除也有效减少了因惰性删除带来的对localStorage空间的浪费。

(1) 删除所有过期的key

(2) 限制每次删除的个数

var lsc = (function (self) {
  var prefix = "one_more_lsc_";
  // 定时的键值对
  self.list = [];
  self.init = function () {
    var keys = Object.keys(localStorage);
    var reg = new RegExp("^" + prefix);
    var temp = [];
    for (let i = 0; i < keys.length; i++) {
      if (reg.test(keys[i])) {
        temp.push(keys[i]);
      }
    }
    self.list = temp;
  };
  self.set = function (key, val, expires) {
    key = prefix + key;
    val = JSON.stringify({
      val: val,
      expires: new Date().getTime() + expires * 1000,
    });
    localStorage.setItem(key, val);
  };
  self.check = function () {
    self.init()
    if (!self.list || self.list.length === 0) {
      return;
    }
    var checkCount = 0;
    while (checkCount < 5) {
      var expireCount = 0; // 如果20个里面过期的不超过5,就结束删除
      for (let i = 0; i < 20; i++) {
        if (self.list.length === 0) {
          break;
        }
        var index = Math.floor(Math.random() * self.list.length);
        var key = self.list[index];
        var val = localStorage.getItem(key);
        val = JSON.parse(val);
        if (val.expires < new Date().getTime()) {
          self.list.splice(index, 1);
          localStorage.removeItem(key);
          expireCount++;
        }
        
      }
      if (expireCount < 5 || self.list.length === 0) {
        break;
      }
      checkCount++;
    }
  };
  // 每隔一秒执行一次定时删除
  window.setInterval(self.check, 1000);
  return self;
})(lsc || {});
lsc.check()
lsc.set("token", "Bearxxx", 1);
lsc.set("token1", "Bearxxx", 1);
lsc.set("token2", "Bearxxx", 1);
lsc.set("token3", "Bearxxx", 1);
lsc.set("token4", "Bearxxx", 1);

localStorage的限制

  1. 兼容性。只有IE8以上版本支持
  2. 值限定为String,对于日常使用的JSON需要进行转换
  3. 在浏览器隐私模式下不可读(个人觉得这是处于隐私安全考虑的合理性,不属于限制范畴)
  4. ls本质上是字符串的读取,存储内容过多会消耗内存空间(不过ls本就设计只有5M左右的空间,个人感觉用于简短信息存储比如token)
  5. localStorage不能被爬虫抓取(偏向于本地存储,存储token类身份认证的信息,也不需要被爬虫爬取)

IndexedDB

浏览器提供的本地数据库,可以被网页脚本创建和操作。在用户的浏览器持久化存储数据的方法,IndexDB为生成Web Application提供了丰富的查询能力,使应用在在线和离线时都可以正常工作

不属于关系型数据库,不支持SQL,更偏向于NoSQL

特点

  1. 键值对存储。
  2. 异步。IndexDB操作不会锁死浏览器。
  3. 支持事务。
  4. 同源限制
  5. 储存空间打。

适用场景

页面中一些不常变动的结构化数据,可以使用IndexDB数据库存储在本地,有助于增强页面的交互性能。

缓存

web缓存大致可以分为:数据库缓存,服务器端缓存(代理服务器缓存,CDN缓存),浏览器缓存

浏览器缓存包含:HTTP缓存、indexDB、cookie、localStorage等等

浏览器缓存主要是HTTP协议定义的缓存机制(下文浏览器缓存皆指HTTP缓存)。HTML meta标签

<meta http-equiv="Pragma" content="no-store">

含义是让浏览器不缓存当前页面,但是代理服务器不解析HTML内容,一般应用广泛的是用HTTP头信息控制缓存。

浏览器缓存分为强缓存和协商缓存。

强缓存

强缓存是利用http的返回头中的Expires或者Cache-Control两个字段控制,用来表示资源的缓存时间。

缓存过期时间,指定资源到期的时间。expires = max-age + 请求时间,需要和last-modified结合使用。但是cache-control的优先级更高。expires是web服务器的响应消息头字段,在响应HTTP请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据。

expires返回的是绝对时间,是根据服务器端时间给的,修改客户端本地的时间,会引起缓存混乱。

cache-control是相对时间,相对于客户端的时间,在请求之后的3600s内有效。

协商缓存

若在强缓存中未找到http缓存,即未命中缓存,浏览器会将请求发送给服务器。服务器根据http头信息中的last-modify/if-modify-since/if-none-match来判断是否命中协商缓存。如果命中,http返回码304,浏览器从缓存中加载资源。

事件循环event loop

主线程从任务队列中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环。

基本概念

调用栈:函数执行的地方,是主线程在运行javascript代码的过程中形成的,遵循后进先出的规则。正在调用栈中执行的函数如果还调用了其他函数,那么这些幻术也将会被添加进调用栈并执行。当函数执行完毕后,会被移出调用栈。

同步任务:在主线程排队执行的任务。

异步任务:不立即执行,在未来某一刻执行的任务,不进入主线程,暂时挂起。当有结果时,将回调函数添加进任务队列。

宏任务:script,定时器,事件回调,UI渲染,I/O等

微任务:Promise对象,process.nextTick,Object.observe,MutationObserver等

任务队列机制:

  • 事件循环中有一个或多个任务队列,任务队列是task的有序列表,task是调度Events,Parsing, Using a resource,Reacting to DOM manipulation的算法。
  • 每个任务都来自一个特定的任务源(比如鼠标键盘事件)。来自同一个任务源且属于特定事件循环的任务必须被加入到同一个任务队列中,来自不同任务源的任务可以放在不同的任务队列。
  • 浏览器调用这些任务队列中的任务时,相同队列按照先进先出顺序,不同队列安装设置的队列优先级
  • 调用任务的过程会产生新的任务,浏览器不断执行任务,因此称为事件循环

浏览器执行任务顺序

  1. 从上到下
  2. 同步任务
  3. 异步任务加入任务队列
  4. 每次执行一个宏任务前,清空微任务队列
  5. 234事件循环

垃圾回收机制

浏览器的javascript具有自动垃圾回收机制(GC:garbage Collection),执行环境会负责管理代码执行过程中使用的内存。

原理:垃圾收集器会定期找出不再使用的变量,释放其内存。由于开销较大,且阻塞其他操作,因此不是实时的,而是周期性的。

基本概念

垃圾:当对象(在堆中)没有变量或属性(在栈中)对其进行引用,此时将无法操作该对象。垃圾过多会占用大量内存,导致程序运行变慢。

内存溢出:一种程序运行出现错误。当程序运行需要的内存超过剩余的内存时,会抛出内存溢出的错误。

内存泄漏:占用的内存没有及时释放,内存泄漏积累会引起内存溢出。

内存泄漏常见类型:

  • 全局。全局变量与全局绑定事件
  • 定时器。没有解除绑定。
  • DOM删除时没有解绑事件。对同一个事件重复监听,忘记移除。
  • 闭包。闭包未释放

触发垃圾回收:垃圾回收是周期性的,但当分配内存多,回收工作艰巨,确定垃圾回收时间间隔是值得思考的问题。

垃圾回收机制

1. 标记清除

当变量进入执行环境,将其标记为“进入环境”,从逻辑上讲,永远不能释放进入环境的变量所占用的内存,当变量离开环境,将其标记为“离开环境”

function test() {
  var a = 10  // 被标记“进入环境”
  var b = 20  // 被标记“进入环境”
}
test()  // 执行完毕之后,a,b标记为“离开环境”,被回收

垃圾回收器在运行时会给存储在内存中的所有变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包);在此之后被加上标记的变量视为将要删除的变量;最后垃圾回收器完成内存清除工作,销毁带标记的值释放内存空间。

GC时停止响应其他操作,对于连贯性要求较高的应用,需要避免GC造成的长时间停止响应。

优化策略
  1. 分代回收

是V8主要采用的,通过区分“临时”和“持久”对象,多回收“临时对象”区,少回收“持久对象”区,减少每次需遍历的对象,从而减少GC的耗时。

持久对象对应主垃圾回收器:遍历过程中,可达的对象进行标记,最后没有标记的判为垃圾数据

临时对象区对应副垃圾回收器,分为两个区域:对象区域和空闲区域。新加入的对象被放入对象区域,等对象区域快满时,会执行一次垃圾清理。

副垃圾回收器清理过程:1) 给对象区域所有对象进行标记;2) 把标记为可达的对象复制到空闲区域,并有序地排列一边;3) 把对象区域和空闲区域对调。

  1. 增量回收

将垃圾回收工作分成更小的快,每次处理一点,多次处理,避免长时间的停顿

2. 引用计数

跟踪记录每个值被引用的次数,被引用次数+1,值引用的变量引用其他的值,则被引用次数-1。当引用次数变为0,则说明无法再访问该值,回收其内存空间。

function test(){
  var a = {}  //num:0 
  var b = a  // num:1
  var c = a  // num:2
  var b = 1  // num:1
  var c = {} // num:0  回收
}
  
循环引用
function fn(){
  var a = {}
  var b = {}
  a.pro = b
  b.pro = a
}
fn()

上述a和b的引用次数均为2,但是函数fn执行完毕后,ab离开环境,无法被回收。(老版本IE会存在这样的问题)

需要手动解除循环引用

myObject.element = null
element.o = null
window.onload = function outerFun() {
  var obj = document.getElementById('element')
  obj.onclick = function innnerFun(){}
  obj = null // 手动断连接

身份认证

避免在客户端频繁向服务器请求数据时,服务器频繁比对数据库中的账户密码

简单组成:uid(用户唯一标识),time(时间戳),sign(签名)

token认证流程

  1. 使用username,password请求登录
  2. 服务器验证username,password
  3. 验证成功,向客户端签发token
  4. 客户端收到token,存储起来,cookie或者localStorage里
  5. 之后发起请求,带着token(token放进http请求头)
  6. 服务端验证请求里的token,验证成功返回数据
  7. 用户注销,需手动清除token

特点及缺陷

  1. 无状态机制。不会在数据库或服务器中有保存用户的任何信息
  2. 安全问题。请求中发送 token 而不是 cookie,这能够防止 CSRF攻击。
  3. 可扩展性。使用 Tokens 能够与其它应用共享权限。使用 token,可以给第三方应用程序提供自定义的权限限制。当用户想让一个第三方应用程序访问它们的数据时,我们可以通过建立自己的API,给出具有特殊权限的tokens。
  4. 多平台与跨域。
  5. 基于标准。最常用的:JSON Web Tokens

cookie与session身份认证

  1. 登录请求
  2. 服务器验证凭据,创建session,可以存储在内存或者数据库中,为了更好的扩展,建议将其存储在数据库中。如果存储在内存中,在使用负载均衡或多服务器部署的场景下会出现session问题。
  3. 服务器通过将cookie包含在Set-Cookie中来响应浏览器。cookie通过键值对发送,包含唯一的sessionID表示用户。
  4. 浏览器存储cookie,在后续请求中一起发送。服务器收到后,将cookie中的sessionID与数据库中的session进行比较验证用户的有效性。
  5. 用户注销后,服务器删除session

特点及缺陷

  1. 自动化过程。发起请求浏览器自动带cookie
  2. 虽然对用户信息加密,但是浏览器有操作cookie的API,所以不安全,容易受到XSS和CSRF攻击(可以设置HttpOnly来保护cookie免受XSS攻击)。
Set-Cookie: <cookie-name>=<cookie-value>;Secure
Set-Cookie: <cookie-name>=<cookie-value>;HttpOnly
// 防CSRF
Set-Cookie: <cookie-name>=<cookie-value>;SameSite=Lax
  1. 同源