前言

以下性能优化的手段是主流的、面向Chrome的,但适用于大部分浏览器。

渲染引擎组成

一个渲染引擎主要包括:

  1. HTML解析器:用于解析HTML文本,将文本解析为DOM树。
  2. CSS解析器:用于解析CSS文本,构建CSS树。
  3. JS引擎:用于执行HTML中script标签内的代码,可以通过浏览器的API操作DOM元素。
  4. 布局模块:在将DOM树和CSS树合并为Render树后,布局模块计算每个元素的位置和大小。
  5. 绘图模块:绘制页面。

    大致的解析与渲染流程⭐⭐

  6. 调用HTML解析器解析HTML文本(包括HTML标签、SVG、XHTML),目标是构建一棵DOM树。

  7. 遇到style标签,异步地解析style标签内的CSS代码,并更新CSS树。

    内联的CSS代码不阻塞Render Tree生成与页面初次渲染。

  8. 遇到link stylesheet,异步地请求资源,在CSS文件被返回时,异步地调用CSS解析器解析文件中的CSS代码,并更新CSSOM(CSS Object Model)。

    不同于style标签,外链CSS样式表的下载与解析会阻塞Render Tree的生成与后续的页面初次渲染。

  9. 遇到普通 script标签,同步地请求资源,在JS文件被返回时,同步地执行文件中的JS代码。

  10. HTML解析完成后生成一棵DOM树,CSSOM构建完成后,将CSSOM和DOM树合并为Render Tree

    渲染树不是简单地将二者合并,它只会包含可视节点与其样式信息。

  11. 根据渲染树中的尺寸样式和布局样式,计算每个节点的几何信息(位置和形状)

    初次渲染时这个环节可以叫做“布局”,再次触发这个过程就称为回流 reflow或重排。

  12. 根据外观样式+布局的结果,将节点绘制到图层位图上 👈 再次触发这个过程就称为重绘。

    初次渲染时这个环节可以叫“绘制”,再次触发这个过程就称为重绘repaint

  13. 将像素图层发送给GPU,显卡发送信号展示页面。

生成渲染树

image.png

为了构建渲染树,浏览器主要完成了以下工作:

  1. 从DOM树的根节点开始遍历每个可见节点。
  2. 对于每个可见的节点,找到CSSOM中对应的规则,并应用它们。
  3. 根据每个可见节点以及其对应的样式,组合生成渲染树。

    ⭐ 由此可见,构建渲染树是一个相当复杂且费时的工作。 所以我们在构建页面时,要注意:

    1. DOM树要小,层级不宜过深。
    2. CSS选择器尽量用id和class,层级不宜过深。

第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。
不可见的节点包括:

  • 一些不会渲染输出的节点,比如script、meta、link等。
  • 一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。

    再次强调,渲染树中只包含可见的节点。

阻塞与非阻塞⭐⭐

CSS阻塞

  1. 关于style标签内的CSS
    例如:
    1. <style>
    2. * {
    3. margin: 0;
    4. padding: 0;
    5. }
    6. </style>
  • 不阻塞DOM解析
  • 不阻塞DOM渲染(style标签内的CSS代码是异步解析更新CSS树的)
  1. 关于link标签引用的外部样式表
    例如:
    1. <link rel="stylesheet" href="common.css">
  • 外部样式表的下载不阻塞DOM解析,HTML解析器在解析到外部引用CSS时,会发出异步请求,得到响应后使用CSS解析器异步解析该文件(相较于HTML解析)。
  • 外部样式表的下载和解析阻塞DOM渲染,即只要有link标签引用的外部资源没有加载或加载了但CSS解析器没完成解析,DOM不会完成渲染,页面不显示内容,尽管这时候DOM解析可能已经完成了。
  • 外部样式表的下载和解析阻塞后面的JS脚本执行,即只要script标签前面的有引用的外部样式表没有加载、解析完成,则script标签内的JS脚本不会执行。

    优化手段:

    • 小:优化CSS代码,CSS文件压缩合并。
    • 早:通过preload资源暗示、HTML中link标签位置放置于JS脚本之前等。
    • 近:利用CDN节点。

    ⭐ 目的都是尽可能快的下载CSS文件。

JS阻塞

  1. 普通的内联JS代码
    例如:
    1. <script>
    2. console.log('foo');
    3. </script>
  • 内联的JS脚本的执行阻塞DOM解析,即HTML解析器会停下来等待脚本执行完毕。
  • 由于阻塞DOM解析,所以一定阻塞DOM渲染(渲染的必要条件是DOM解析完成,构建DOM树)。
    1. 普通的外部JS资源
      例如:
      1. <script src="main.js"></script>
  • 外部JS资源的下载会阻塞DOM解析
  • 外部JS资源的执行也会阻塞DOM解析

    即HTML解析器遇到这样的一个外部JS引用标签,会等待该JS文件下载成功、执行完毕后,再继续解析下面的HTML代码。

  1. defer / async script标签
    例如:
    1. <script defer src="foo.js"></script>
    2. <script async src="bar.js"></script>
  • async script 的下载是异步的,不阻塞DOM解析,也不阻塞渲染。注意,是JS脚本的下载任务是不阻塞DOM解析和渲染的。
  • async script 的执行可能会阻塞DOM解析,因此可能阻塞渲染。如果下载完成的时间点在DOM解析完成之前,则该脚本的执行会阻塞,如果下载完成的时间点在DOM解析完成之后,则不阻塞DOM渲染。
  • async script 是乱序执行的,只要下载完成且不被CSS解析阻塞就执行,不考虑与其他async script的顺序关系。

    async script是乱序的,所以可以用于链接那些不依赖于其他脚本、且不被其他脚本依赖的JS代码文件。

    async script的执行会中断DOM解析,所以async script中最好不要操作非该脚本创建的DOM元素,因为此时HTML解析到了哪个元素是不确定的。

    ❓ 有一个问题:如果async script下载完成的时机是在DOM解析完成之后,DOM渲染之前,那这个JS脚本阻不阻塞DOM渲染?

  • defer script 的下载是异步的,不阻塞DOM解析,也不阻塞渲染。

  • defer script 的执行不阻塞DOM解析。最早的执行时间点是DOM解析完成之后,DOMContentLoaded事件之前。
  • defer script 的执行可能阻塞DOM渲染。如果defer script在DOM解析完成之前下载完成,则将在DOM解析完成之后进行执行,则可能阻塞DOM渲染。
  • defer script 是顺序执行的。H5规范规定defer scripts 之间的执行顺序要严格按照在HTML文件中的位置,从上到下执行。

    一般情况下,defer script更能满足应用脚本的使用场景。

    ❓ 这里同样有这样一个问题:如果defer script下载完成的时机是在DOM解析完成之后,DOM渲染之前,那这个JS脚本阻不阻塞DOM渲染?

image.png

优化手段:

  • 通过defer属性的JS引用避免阻塞DOM解析。
  • 利用prefetch属性加载非必要但可能会用到的JS文件。

图层

基于图层渲染

更具体来说,浏览器在渲染一个页面时会将页面分成一到多个图层:

  1. 基于HTML解析+CSS解析的结果,这个过程中会根据DOM元素的特性将所有元素分为一到多个图层,而DOM树和CSS树合成为渲染树。

    我理解为:渲染树的节点信息中包含这个元素属于哪个图层,即图层信息包含在渲染树内。

  2. 为每个节点计算位置、形状(回流)。

  3. 将每个DOM节点绘制填充到图层位图中(重绘)。
  4. 图层作为纹理上传到GPU。
  5. GPU组合多个图层,进行叠加等操作,最后将结果展示到屏幕上。

创建图层

基于Chrome进行测试,以下情况会创建图层:

  1. 所有页面有一个document图层,一般的DOM元素绘制于该图层上。
  2. 拥有3D变换CSS属性transform的DOM节点会单独开辟一个图层。
  3. 应用CSS3动画的节点animation: keyframeName
  4. 使用加速视频解码的<video>节点会开辟一个图层。
  5. <canvas>节点会开辟一个图层。
  6. 拥有CSS加速属性的元素(will-change)。
    1. #animate {
    2. will-change: transform;
    3. }
  1. ❓ 不确定。Chrome中固定定位fixed和粘滞定位stick的元素好像也会单独设置为一个图层。

回流与重绘⭐⭐⭐

回流 reflow

基于更新后的渲染树,将可见DOM节点与其布局样式、尺寸结合起来,计算出它们在布局视口中的确切位置和形状大小,这个计算的阶段就是回流。

重绘 repaint

我们通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,和他们的具体几何信息(位置、大小),那么我们就可以再根据外观样式,将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘。

重绘是以图层为单位的,如果图层中某个元素需要重绘,则整个图层都需要重绘。 所以为了提高性能,我们应该让这些经常会重绘的元素拥有一个自己的图层。

触发回流的情况

  • 添加或删除可见的DOM元素(包括设置display:none或由none转为其他值) 。
  • 元素的位置发生变化。
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 页面的布局视口变化(特别是PC端窗口resize,因为回流是根据视口的大小来计算元素的位置和大小的)。
  • 字体发生变化。
  • …..

大部分情况下,重排是以Render Tree为单位的,因为如果某个独立图层上的元素触发了重排,而该图层的元素没有脱离文档流,则单独重排该图层是达不到效果的。

具体而言,以下CSS属性发生改变会触发回流:

image.png

同时,通过Web APIS访问以下CSS样式会触发回流:

image.png

只要是获取元素位置相关的操作,都会触发回流,原因在于:

现代的浏览器是经过优化的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列进行一次回流/重绘,以减少回流/重绘次数(类似于节流)。但是!当你每次获取布局信息的操作的时候,会强制队列刷新,进行一次回流/重绘,以得到最新最准确的信息

以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新触发回流的操作队列,强制进行一次回流计算。如果要使用它们,最好将值缓存到JS变量中。

触发重绘的情况

image.png

总结为:

  • color
  • background 简写属性及其相关具体样式。
  • 一些装饰性的样式,如border-radius、border-style、text-decoration、box-shadow、outline相关。
  • visibility

优化方案

  1. 在兼容性良好的情况下,移动元素位置时,使用transform,替代操作left/top

    transform的改变仅仅是改变图层的组合方式,而不触发重排和重绘。

  1. 使用opacity: 0;/ opacity: 1;代替visibility: hidden;/ visibility: visible;
    • 使用visibility不触发重排,但是依然重绘。
    • 直接使用opacity既触发重绘,又触发重排。
    • 理论上,改变一个独享图层的元素的opacity属性,既不触发重排,也不触发重绘。

      这里有点小问题,最新版本的Chrome测试的结果是配合图层使用时,opacity也会触发重绘。

  1. 利用元素的class,将多个样式的改变合并成一次操作(修改元素类名)。
  2. 将DOM元素脱离DOM树后再修改。

    DOM元素不在DOM树中意味着元素不在渲染树中,这有两种情况:

    • 元素不在DOM树中,就不可能存在于渲染树中。
    • 元素在DOM树中,但display: none;

如果要对一个元素进行复杂的样式操作时,可以先不插入DOM树、或者先对该树中元素使用display: none;,操作完样式后再使之可见。这样至多触发2次重排重绘。

  1. 利用文档碎片documentFragment。Vue中就利用了该方式提升渲染性能。
  2. 避免频繁地通过JS访问节点的位置、尺寸等样式属性,必要时进行缓存。

其他关于动画的优化手段:

  1. 当通过背景图片位置的变化实现动画效果时,记得为该元素开启一个图层,最简单的方式:transform: translateZ(0)
  2. 不懂❓ 为动画元素新增图层,提高动画元素的z-index

Event Loop 与 渲染⭐

关于 Event Loop 的文章很多,但是有很多只是在讲「宏任务」、「微任务」,我先提出几个问题:

  1. 每一轮 Event Loop 都会伴随着渲染吗?
  2. requestAnimationFrame 在哪个阶段执行,在渲染前还是后?在 microTask 的前还是后?
  3. requestIdleCallback 在哪个阶段执行?如何去执行?在渲染前还是后?在 microTask 的前还是后?
  4. resizescroll 这些事件是何时去派发的。

这是一篇博客提出的问题👉原文

事件循环与页面渲染的流程

注意:同一时间,JavaScript脚本执行与页面渲染互斥。

  1. 从任务队列中取出一个宏任务执行。
  2. 检查微任务队列,执行并清空微任务队列,如果在微任务的执行中又加入了新的微任务,也会在这一步一起执行。
  3. 清空微任务队列后,判断是否允许渲染(每次渲染间有一个最小时间间隔阈值,这个阈值与用户屏幕刷新率、页面性能状态等有关)。

    浏览器会尽可能的保持帧率稳定,例如页面性能无法维持 60fps(每 16.66ms 渲染一次)的话,那么浏览器就会选择 30fps 的更新速率,而不是偶尔丢帧。

  1. 如果允许渲染,就这是一个rendering oppotunity,再判断是否需要渲染:
    • 考虑更新渲染是否会带来视觉上的改变?如果是,则渲染。
    • 观察map of animation frame callbacks是否为空,即是否使用了requestAnimationFrame注册了回调。如果是,则渲染。

如果上述两个条件都不满足,则浏览器将跳过接下来的渲染流程。

  1. 对于需要渲染的文档,如果窗口的大小发生了变化,执行监听的 resize 方法。
  2. 对于需要渲染的文档,如果页面发生了滚动,执行 scroll 方法。

❓ 对于5和6,也不清楚具体的意思,但是好像scroll和resize事件的回调有比较高的优先级。

  1. 对于需要渲染的文档,执行帧动画回调,也就是执行requestAnimationFrame 的回调。
  2. 对于需要渲染的文档, 执行 InterpObserver 的回调。
  3. 对于需要渲染的文档,重新渲染绘制用户界面。 ⭐⭐ 终于轮到正式渲染页面一次。
  4. 判断宏任务队列和微任务队列是否都为空,如果是的话,则进行 Idle 空闲周期的算法,判断是否要执行 requestIdleCallback 的回调函数。

requestAnimationFrame⭐⭐

使用requestAnimationFrame实现动画效果。

MDN解释:window.requestAnimationFrame(fn)告诉浏览器——你希望执行动画,并且要求浏览器在下一次重绘之前调用指定的回调函数更新动画。

  • 该方法需要传入一个回调函数fn作为参数,该回调函数会被注册,并在浏览器下一次重绘之前执行,执行完毕后会从注册表中清除,如果想实现连续的动画效果,fn中需要重新调用requestAnimationFrame
  • 该方法返回一个ID,该ID可以用于解除回调函数的注册(使用cancelAnimationFrame(ID))。
  1. let distance = 0;
  2. function move() {
  3. let elm = document.getElementById('foo');
  4. elm.style.transform = `translateX(${++distance})`;
  5. id = requestAnimationFrame(move); // 更新id,JS会阻塞DOM渲染。
  6. }
  7. let id = requestAnimationFrame(move); // 页面第一次渲染时会发生重绘,动画便开始了。
  8. setTimeout(() => cancelAnimationFrame(id), 2000); // 👈2s 后取消动画

requestAnimationFrame注册的回调函数会在下一个render oppotunity时执行,不同用户屏幕的刷新率不同,高刷屏幕执行该回调函数的频率会更大。

CDN⭐

CDN的全称是Content Delivery Network,即内容分发网络。其目的是通过在现有的Internet中增加一层新的网络架构,将网站的内容发布到最接近用户的“边缘节点”,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。CDN有别于镜像,因为它比镜像更智能,或者可以做这样一个比喻:CDN=更智能的镜像+缓存+流量导流。因而,CDN可以明显提高Internet网络中信息流动的效率。从技术上全面解决由于网络带宽小、用户访问量大、网点分布不均等问题,提高用户访问网站的响应速度。

————————————————
版权声明:本文为CSDN博主「xiangzhihong8」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiangzhihong8/article/details/83147542

CDN相关技术

CDN的实现需要依赖多种网络技术的支持,其中最主要的包括负载均衡技术、动态内容分发与复制技术、缓存技术等。

负载均衡技术
负载均衡技术不仅仅应用于CDN中,在网络的很多领域都得到了广泛的应用,如服务器的负载均衡、网络流量的负载均衡。顾名思义,网络中的负载均衡就是将网络的流量尽可能均匀分配到几个能完成相同任务的服务器或网络节点上,由此来避免部分网络节点过载。这样既可以提高网络流量,又提高了网络的整体性能。在CDN中,负载均衡又分为服务器负载均衡设备和服务器整体负载均衡(也有的称为全局负载均衡设备)。服务器负载均衡是指能够在性能不同的服务器之间进行任务分配,既能保证性能差的服务器不成为系统的瓶颈,又能保证性能高的服务器的资源得到充分利用。而服务器整体负载均衡允许Web网络托管商、门户站点和企业根据地理位置分配内容和服务。通过使用多站点内容和服务来提高容错性和可用性,防止因本地网或区域网络中断、断电或自然灾害而导致的故障。在CDN的方案中全局负载均衡设备将发挥重要作用,其性能高低将直接影响整个CDN的性能。

智能DNS服务器的核心就是根据负载均衡策略导流不同来源的请求。

动态分发与复制技术
众所周知,网站访问响应速度取决于许多因素,如网络的带宽是否有瓶颈、传输途中的路由是否有阻塞和延迟、网站服务器的处理能力及访问距离等。多数情况下,网站响应速度和访问者与网站服务器之间的距离有密切的关系。如果访问者和网站之间的距离过远的话,它们之间的通信一样需要经过重重的路由转发和处理,网络延误不可避免。一个有效的方法就是利用内容分发与复制技术,将占网站主体的大部分静态网页、图像和流媒体数据分发复制到各地的加速节点上。所以动态内容分发与复制技术也是CDN所需的一个主要技术。

个人理解:CDN上要能够存储一些静态资源,这里也存在推拉模型❓

缓存技术
缓存技术已经不是一种新鲜技术。Web缓存服务通过几种方式来改善用户的响应时间,如代理缓存服务、透明代理缓存服务、使用重定向服务的透明代理缓存服务等。通过Web缓存服务,用户访问网页时可以将广域网的流量降至最低。对于公司内联网用户来说,这意味着将内容在本地缓存,而无须通过专用的广域网来检索网页。对于Internet用户来说,这意味着将内容存储在他们的ISP的缓存器中,而无须通过Internet来检索网页。这样无疑会提高用户的访问速度。CDN的核心作用正是提高网络的访问速度,所以,缓存技术将是CDN所采用的又一个主要技术。

使用CDN的好处

对于用户:

  1. 解决跨运营商和跨地域访问的问题,最重要的是大大降低获取网络资源的延迟,用户体验更好。

对于企业、网站:

  1. 大部分请求再CDN边缘节点完成,起到了分流作用,减轻了源站负载。
  2. 提高网站的稳定性、可用性。
  3. 让大规模的用户请求架构变得简单了,避免企业自己去部署很多节点。

大致工作流程

当用户通过浏览器访问已经加入CDN服务的网站时,首先通过智能DNS(有博客说叫做DNS重定向)确定最接近用户的最佳CDN节点,同时浏览器将请求发送到该节点。当用户的请求到达指定节点时,CDN的服务器(节点上的高速缓存)负责将用户请求的内容响应给用户。

从以上内容可以分析出,CDN技术的主要组成部分之一是DNS接管。

首先,让我们看一下传统的未加缓存服务的访问过程:

image.png

如图可以看出,传统的网络访问的流程如下:

  1. 用户输入访问的域名,操作系统向 LocalDns 查询域名的ip地址;
  2. LocalDns向 ROOT DNS 查询域名的授权服务器(这里假设LocalDns缓存过期);
  3. ROOT DNS将域名授权dns记录回应给 LocalDns;

    这里是一个不断查找目标域名的NS纪录的过程,可能问询了不止一台授权服务器。

  1. LocalDns得到域名的授权dns记录后,继续向域名授权dns查询域名的ip地址;

    最终的授权DNS服务器上有目标域名的A纪录,即目标域名的ip地址。

  1. 域名授权dns 查询域名记录后,回应给 LocalDns;
  2. LocalDns 将得到的域名ip地址,回应给用户端;
  3. 用户得到域名ip地址后,访问站点服务器;
  4. 站点服务器应答请求,将内容返回给客户端。

下面让我们看一下使用CDN缓存后的网站的访问过程:

image.png

如上图,是使用CDN缓存后的网络访问流程:

  1. 用户通过浏览器访问网站,浏览器调用操作系统接口向 LocalDns 查询域名的ip地址。

    这里存在以下假设前提:

    • 浏览器域名缓存不存在该域名IP。
    • 操作系统的DNS解析缓存不存在该域名IP。
    • 本地host文件中没有该域名+ip的键值对。
  1. LocalDns向 ROOT DNS 查询域名的授权服务器。

    这里假设LocalDns缓存过期。

  1. ROOT DNS将域名授权dns记录回应给 LocalDns;

    ❓ 域名授权DNS会不会是一台智能DNS服务器?从而减少4~6步?

  1. LocalDns得到域名的授权dns记录后,继续向域名授权dns查询域名的ip地址;
  2. 域名授权dns 查询域名记录后(一般是CNAME),回应给 LocalDNS;

    DNS服务器的表中有A记录、NS记录、CNAME记录。

  1. LocalDNS重新到ROOT DNS查询域名别名的授权DNS服务器IP(智能DNS服务器的IP)。
  2. LocalDNS 得到智能调度DNS的IP后,向智能调度DNS查询域名的ip地址;
  3. 智能调度DNS 根据一定的算法和策略(比如静态拓扑,容量等),将最适合的CDN节点ip地址回应给LocalDNS。
  4. LocalDNS将得到的域名ip地址,回应给用户端;
  5. 用户浏览器得到域名ip地址后,访问站点服务器。

综上,CDN网络是在用户和服务器之间增加Cache层,主要是通过网站接管DNS解析实现,将用户的请求引导到Cache上获得源服务器的数据,从而降低网络的访问时间。

CDN通过“推拉”更新内容

CDN分为推拉两种方式,推是源服务器将内容推到cdn节点上,拉是cdn在第一次接受请求的时候从服务器拉取资源进行响应并保存,当资源在cdn缓存之后,如果服务器上的资源发生变化,cdn节点是不会知道的,除非缓存时间到期重新拉取或者修改新资源的访问地址。

防抖和节流⭐⭐⭐

防抖 debounce

概念: 延迟要执行的动作,若在延迟的这段时间内,再次出发了,则取消之前开启的动作,重新计时。

实现: 借助setTimeoutclearTimeout

应用场景: 搜索时等待用户完整输入内容后再发送查询请求。

function debounce(fn, delay = 300) {
    let timer;
    return function(...args) {
       timer && clearTimeout(timer);
       timer = setTimeout(() => {
           fn.apply(this, args);
           clearTimeout(timer);
           timer = undefined;
       }, delay);
    }
}

节流 throttle

概念: 设定一个特定的时间间隔,让函数在此事件间隔内只执行一次,不会频繁执行。

实现: 借助一个标识和setTimeout

function throttle(cb, interval = 300) {
    let allowed = true;
    return function(...args) {
        if (allowed) {
            allowed = false;
            setTimeout(() => { allowed = true; }, interval);
            cb.apply(this, args);
        }
    }
}

联系和区别

联系:防抖和节流的目的都是当短时间内同一事件多次触发时,避免多次执行同样的回调。

区别:

  • 防抖是一段时间内重复触发回调,下一次取消上一次的再执行。
  • 节流是一段时间内重复触发回调,只要保证最先的一次执行即可。

Web Storage ⭐⭐

Cookie、LocalStorage、SessionStorage这三者都可以被用来在浏览器端存储数据,而且都是字符串类型的键值对。

Web Storage主要包括localStoragesessionStorage,二者都有大约5M的空间。

增删改API

  1. 保存数据。
    localStoragge.setItem('key', 'value')
    localStorage.setItem('obj', JSON.stringify(obj))
    sessionStorage.setItem('key', 'value')
    sessionStorage.setItem('obj', JSON.stringify(obj))

    ❗⭐本地存储中,存储的value一定是字符串类型⭐❗。如果调用setItem方法,value为基本类型时,API会自动转换为字符串,而对于引用类型,必须显示调用JSON.stringify后再存入。

  1. 读取数据,结果一定是字符串。
    localStorage.getItem('key')
    sessionStorage.getItem('key')

    如果不存在键名为key的记录,则返回null。

  1. 删除一条记录。
    localStorage.removeItem('key')
    sessionStorage.removeItem('key')
  2. 清空所有键值对。
    localStorage.clear()
    sessionStorage.clear()

window的storage事件

作用

我们可以通过监听window对象的storage事件来实现跨页签通信(数据同步)。

window.addEventListener('storage', function(event){...})

触发机制

  1. 该事件会在web storage(localStorage/sessionStorage)的内容发生变化时触发(例如创建、删除、修改、清空操作)。
  2. 在某个页面中修改了web storage的内容,在本页面并不会触发window对象的storage事件,而是在其他共享的页面中触发。

StorageEvent对象

window对象的storage事件的回调函数中,event的实参是一个StorageEvent对象,该对象具有以下属性:

  • key: 新增、更新、删除的记录键名,调用的是clear()时,该属性值为null。
  • newValue: 最新的值,如果调用clear()或removeItem(),该属性值为null。
  • oldValue: 修改之前的值,如果是新增了该键值对,该属性值为null。
  • url: 触发该事件时的页面的url。
  • storageArea: 当前的storage对象,即 localStorage或 sessionStorage。

以上属性都是只读属性。

有效期和作用域

localStorage 作用域

限定在文档源级别的,每个源只能访问属于自己的localStorage,不同的path之间共享。

文档源通过协议、主机名、端口三者来确定(即每个源拥有一个localStorage)。

sessionStorage 作用域

限定在文档源级别,一个标签页下的每个源只能访问属于自己的sessionStorage,不同的path之间共享,但不同页签之间同一个源的sessionStorage不共享。

例如:第一个标签页打开www.baidu.com,写入一些东西到sessionStorage,再新建一个标签页打开www.baidu.com,新页签下www.baidu.com的sessionStorage和第一个页签www.baidu.com的sessionStorage不相同。

localStorage 有效期

某个源下的localStorage永不失效,除非web应用删除一条或全部的记录,或者浏览器清空本地存储。

sessionStorage 有效期

标签页关闭或整个浏览器程序窗口关闭后失效,单个标签页内的跳转、前进、后退或刷新不会使得会话期间某个源下的sessionStorage失效。

但需要注意的是,如果在本标签页上点击了a标签,并使用新的标签页打开链接,即使新的标签页和本标签页是同源的,在新的标签页上也访问不到原标签页的sessionStorage。

Cookie

由于HTTP协议是无状态的,而服务器端的业务必须是要有状态的。Cookie诞生的最初目的是为了在客户端存储Web中的状态信息,以方便服务器端使用,例如:用于判断用户是否是第一次访问网站。

HTTP是无状态的:HTTP通信中,每个请求/响应都是完全独立于其他的请求/响应的。每个请求包含了处理这个请求所需的完整的数据,每个响应也只是针对请求做出的回答。请求/响应的过程不涉及到状态变更。

Cookie的处理

  1. 通过HTTP响应头中的Set-Cookie首部,服务器向客户端发送cookie。
  2. Set-Cookie首部只能在客户端设定一条cookie,如果需要设置多条Cookie,则需要在一个响应头中包含多个Set-Cookie首部。
  3. 根据有效期属性,Cookie在浏览器上保存一段时间或删除某条Cookie。
  4. 之后每次Http请求,浏览器都会自动将对本次请求可见的、未过期的、被允许的Cookie发送给服务器。

Cookie的结构 ⭐⭐

  • Name/Value
    NameValue是一个键值对。Name是Cookie的名称,Cookie一旦创建,名称便不可更改,一般名称不区分大小写;Value是该名称对应的Cookie的值,如果值为Unicode字符,需要为字符编码。如果值为二进制数据,则需要使用BASE64编码。

    HTTP头部中只允许出现ASC字符,所以Cookie中只允许出现ASC字符。

  • Domain
    Domain决定Cookie在哪些域名下的主机是有效的,也就是决定在向该主机发送请求时是否携带此Cookie,Domain的设置在一些情况下是对子域生效的,如Domain设置为.a.com,则该域名下的主机b.a.com和主机c.a.com均可使用该Cookie,同时.xxx.a.com域名下

    需要强调:

    1. 如果某个域下的Cookie如果希望能够被他的子域具有可见性、可用性,应该保证这个cookie在被Set的时候,应该以”.”开头。不同浏览器有不同样的实现,但这样做一定不会错。例如,有些浏览器认为Cookie的Domain不以点开头,只有请求的主机名和Domain相同时,才会发出该Cookie,而IE不是这样的。
    2. 如果服务端在Set Cookie时,如果省略Domain属性,那么大部分浏览器会将该条Cookie的domain设置HTTP请求中的host值(主机名)。
    3. Cookie的Domain不支持端口(❓ 不确定支不支持IP),如果你想多个端口来区分cookie的域,可以为这些不同端口的访问来绑定不同的子域名。

❓ 有一个疑惑:

对子域名下的主机发出请求时,父级域名下的Path不为/的Cookie是否对该请求可见?

  • Expires/Max-age
    Expires和Max-age均为Cookie的有效期,Expires是该Cookie被删除时的时间戳,格式为GMT。
    Max-age也是Cookie的有效期,但它的单位为秒,即多少秒之后失效,Max-age比Expires优先级更高。
    设置了未过期的Expires值或Max-age>=0的Cookie称为持久化Cookie,会保存至硬盘文件,过期时删除。
    没有设置Max-age,但设置Expires时,以Expires为准。
    既没有设置Max-age,也没有设置Expires的Cookie成为会话Cookie,会保存在内存中,浏览器关闭时删除。

    需要注意:

    1. Cookie的Expires值由服务端设置,该时间戳为服务器时间,而浏览器以本地时间做过期判定。
    2. 若Expires的值为本地过期的时间,则该Cookie立刻被删除。
    3. 若Max-age设置为0,则立刻失效,同Expires过期的情况。
    4. 若Max-age设置为-1,浏览器关闭时,该Cookie会删除,意味着该条Cookie成为会话Cookie。
    5. 如果希望将客户端已有的Cookie删除,则后端可以发送过期的Expires值或者Max-age=0;又或是希望通过Max-age=-1将客户端某条已有的Cookie变为会话Cookie;必须同时指定Cookie的Name、Value、Expires/Max-age,以及Domain和Path,因为Domain+Path+Name才能定位到客户端的一条Cookie。
  • HttpOnly
    HttpOnly为布尔属性,默认时没有该属性,如果设置Cookie时存在该属性,则不允许通过脚本document.cookie去更改这个值,同样这条Cookie在document.cookie中也不可见,但在发送请求时依旧会携带此Cookie。
  • Secure
    Secure为Cookie的安全属性,它是一个布尔属性,默认为false,若Cookie记录中存在该属性则为true,则浏览器只会在HTTPS和SSL等安全协议中传输此Cookie,不会在不安全的HTTP协议中传输此Cookie。
  • SameSite
    Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,用来防止跨站点请求伪造(CSRF)攻击和用户追踪。这个属性也被IEIF提倡使用。当然防止CSRF攻击还有其他方法,如CSRF token校验以及Referer请求头校验。
    SameSite属性可以设置三个值: ① Strict ② Lax ③ Noneimage.png
    1. Strict
    Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送此类型Cookie。换言之,只有当前网页的URL的主机名部分与Cookie的Domain属性值是同站的关系(same-site),请求时才会发送此类型的Cookie。

    Set-Cookie: CookieName=CookieValue; SameSite=Strict;
    


    这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。
    2. Lax
    Lax规则稍稍放宽,跨站点时,大多数情况也是不发送第三方 Cookie,但是导航到与Cookie同站点的 Get 请求除外(如A标签、GET表单、link:prefetch/preload)。
    3. None
    不对此条Cookie在第三方上下文中的发送作限制,只要是Cookie对第三方上下文的这次请求可见,就把Cookie发送过去。
    Cookie的SameSite属性:https://blog.csdn.net/weixin_44269886/article/details/102459425
    SameSite阮一峰:http://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html
    SameSite cookies 理解 https://juejin.cn/post/6844904095476613133

  • Priority
    优先级,chrome的提案,定义了三种优先级,Low/Medium/High,当cookie数量超出时,低优先级的cookie会被优先清除。在360极速浏览器和FireFox中,不存在Priority属性,不清楚在此类浏览器中设置该属性后是否生效。

Cookie的写入

① a标签跳转

② 表单GET/POST

③ 外部资源引用src(图片)

④ iframe src

⑤ Ajax

以上这五种方式,都有可能发出跨域/不跨域、跨站/不跨站的请求,返回的响应头中很可能会有Set-Cookie。

以下开始分析浏览器如果发现各种类型请求的响应头中有Set-Cookie,它会怎样处理。

  1. 如果请求不跨域,就会像顶级导航一样,响应头中任何Set的Cookie都有效,浏览器会对它执行有关逻辑。
  2. 如果这个请求跨域不跨站,响应中Set的Cookie就可以写入,不论Cookie的SameSite属性为何值。

    ❗ 如果是跨域不跨站的AJAX请求,请看第五条。

  1. ⭐即使这个请求不跨站,但Scheme为HTTP,浏览器只允许写入响应头中SameSite=LaxSameSite=Strict的Cookie,不允许写入SameSite=None;Secure的Cookie。
  2. ❓ 如果是跨站请求,浏览器只会写入响应头中SameSite=None;Secure的Cookie。

    不确定是否会存入SameSite=Strict或SameSite=Lax的Cookie。

    ❗ 如果是跨站的AJAX请求,请看第五条。

  1. 如果是AJAX 跨域不跨站/ 跨站请求:
    • 如果XMLHttpRequest对象的withCredentials属性值为false,则响应头中Set的任何Cookie都无效,浏览器不会写入它们。

      withCredentials属性既控制AJAX跨域请求时是否携带Cookie,又控制是否允许该请求的响应在自己的域中设置Cookie,具体可以参考MDN对XMLHttpRequest.withCredentials的解释。

  • 如果XMLHttpRequest对象的withCredentials属性值为true,则跨域/跨站AJAX请求会发送与之相关的Cookie,而对响应头中的Set-Cookie的处理可以参考2~4条。

Cookie的Web API

我们不仅可以通过HTTP响应让浏览器操作Cookie,还可以在页面运行JS脚本时,通过代码操作当前域下可见的Cookie。

受同源政策的约束,在某个页面中以JS脚本的方式只允许访问页面的主机名下的Cookie及其上级域名的Cookie(受限于Domain)。同时只能访问页面所在目录及其上级目录的Cookie(受限于Path)。

例如:

当前访问的页面是https://www.a.com/pathtohtml/index.html,则该页面的脚本只可以访问www.a.com.a.com域名下的Cookie,同时只能访问/pathtohtml//路径下的Cookie。

API:

// 读取当前页面下的所有Cookie,其值为形如`key1=value1;key2=value...`的字符串
console.log(document.cookie);

// 添加一条Cookie
document.cookie = 'key=value;Max-age=300;Path=/;SameSite=None;Secure'

// 删除一条Cookie
document.cookie = 'key=value;Max-age=0;Path=/;'

注意:

  1. Cookie默认不会保存任何数据,document.cookie默认值为空字符串。
  2. Cookie不能一次性设置多条,只能一条一条添加。
  3. 同一个站点,Cookie有个数限制,一般为20~50条,大小限制在4KB。
  4. HTTP响应中设置了HttpOnly的Cookie不能通过document.cookie访问,但确实存在。

第一方Cookie注意事项

第一方Cookie是指:当前网页主机名下可以访问到的Cookie(一般是同站的Cookie)。

  1. 第一方Cookie可以由网站加载的第三方脚本写入,即第三方脚本中使用 document.cookie 接口。
  2. 第一方Cookie如果没有设置HttpOnly,则可以通过跨站外链、跨站AJAX请求将第一方Cookie以参数形式发送给第三方脚本。
  3. 第一方Cookie即使设置了HttpOnly,第三方仍然可以让第一方(网站服务器)托管脚本,从而读取第一方Cookie。

浏览器禁用第三方Cookie

第三方Cookie是指:相对于当前页面URL的主机名,某个请求是跨站请求,发送给该站点以及该请求的响应头中含有的Cookie属于第三方Cookie(即在一次跨站请求中,第三方Cookie含有发送和写入两个环节)。

很多浏览器都具有禁用第三方Cookie的功能,开启禁用后,如果发送跨站请求:

  1. 发送请求环节,不论浏览器在该跨站域名下的Cookie的SameSite属性为何值,都不会发送Cookie。
  2. 得到响应环节,Set-Cookie首部中的任何Cookie都不会被浏览器记录。

区别于SameSite=Strict的Cookie,该属性只是不允许在跨站请求时发送Cookie,这是对用户的Cookie进行保护。❓ 其实不是确定跨站响应中SameSite=Strict的Cookie是否会被写入。

浏览器禁用第三方Cookie功能是为了保护用户的隐私,禁止第三方网站跟踪用户、分析用户行为。

Cookie和Web Storage的区别与联系 ⭐⭐⭐

区别

可以从Cookie、localStorage、sessionStorage的有效期、作用域、大小限制、发送时机来进行回答。

有效期

  • Cookie的有效期分两种,会话Cookie在浏览器程序关闭时删除,持久化Cookie在到期时被删除。
  • localStorage除非Web应用代码删除、用户点击删除、用户清空本地存储,否则一直有效。
  • sessionStorage在页签关闭时删除,历史记录前进后退时也不删除,不同页签打开同一页面不共享。

作用域

  • Cookie的作用域:
    子域名可以访问本级与上级域名的Cookie,反之不可;
    子目录可以访问当前目录与上级目录的Cookie,反之不可。

    不同浏览器不共享Cookie!!!

  • localStorage的作用域为文档源,即同一个源下的多个页面共享一个localStorage。
  • sessionStorage的作用域为一个页签下的一个文档源,页签可以不断跳转从而显示不同的页面,但是一个页签跳转到不同的域名时,sessionStorage会切换为该域下的存储空间。

大小限制

  • Cookie的大小限制为:一个站点可以存储20~50条Cookie,大小限制在4KB。
  • Web Storage的大小限制为5M~8M。

发送时机

  • Cookie的发送时机是向某个站点下的某台主机发送请求时,会携带对于该次请求可见的Cookie。
  • Web Storage中的内容不会自动发送给服务器,需要通过JS脚本发送。

联系

  1. 都是在浏览器端存储状态的手段。
  2. 在一定程度上,都受同源策略保护。

对域名和主机名的理解

.为根域。

有人称.com.net为顶级域名/一级域名,而baidu.comjd.com为二级域名。

又有人称baidu.com为顶级域名/一级域名。

主机 = 主机的英文名字.域名

例如www.baidu.combaidu.com域下的一台叫做www的主机。

例如live.bilibili.combilibili.com域下的一台叫做live的主机。

一个企业开发的网站,使用多少级域名由企业自己决定。

对跨域和跨站的理解⭐

同域名scheme+host+port相同,其他视为跨域。

举例:

image.png

同站点eTLD+1相同,其他视为跨站。

举例:

image.png

注意:

  1. 普通的跨站不考虑scheme,考虑scheme的跨站叫做schemeful same-site
  2. mzleman.github.ioley.github.io跨站,因为.io不是一个eTLD.github.io才是。a.mzleman.github.iob.mzleman.github.io才是同站的。

浏览器缓存⭐⭐⭐

概念

浏览器在本地磁盘/内存中将之前请求的响应存储起来,当页面再次需要发出这些请求时,无需真正地发送请求(可能是真的不发,也可能是发送简单的缓存校验),然后直接从本地获取响应结果。

使用浏览器缓存的好处

  1. 减少请求次数,避免网络通信带来延迟,从而加快浏览器获取资源、数据的速度。
  2. 减轻网站服务器的压力。
  3. 节省带宽、流量。

强缓存和协商缓存

强缓存:

浏览器发出一条请求,服务器在响应头中通过Cache-control:max-age=.../Expires指示浏览器将该请求的响应缓存一段时间,在缓存失效之前,这条缓存为强缓存。

在下一次请求时,如果缓存不不过期,则命中强缓存,返回200 OK (from memory/disk cache)

协商缓存:

针对浏览器的请求,服务器在响应头中设置了Cache-control:no-cache的响应会被浏览器保存为协商缓存。

或者是当一条强缓存过期时,也可以认为该缓存成为协商缓存。

在下一次请求时,如果浏览器保存的是请求的协商缓存,浏览器会发送缓存校验请求(带有If-modified-Since/If-None-Match的条件请求),如果服务器认为客户端缓存仍然可用,则返回304 Not Modified,如果服务端更新了内容,则返回200 ok的完整响应并指示浏览器更新客户端缓存信息。

针对协商缓存的两种情况进行讨论:

  1. Cache-control:no-cache型的协商缓存:Chrome相应发出Cache-Control:no-cache的请求头。
  2. 过期的强缓存:Chrome发出Cache-control:max-age=0的请求头。