跳到主要内容

浏览器相关

Web Worker

浏览器提供的多线程能力,允许开发者在浏览器中创建一个独立于主线程(GUI 渲染线程)的后台线程,用于执行 计算密集型异步任务,从而避免主线程阻塞,提升页面响应性。

  • 基于消息队列:主线程与 Worker 用 postMessage()onmessage 事件通信;
  • Worker 限制:无法访问 DOM,只能进行数据处理、无法访问 window、外部脚步加载需 worker Module;

场景:

  • 大型 JSON 解析、图片压缩 / 滤镜处理,常配合 WebAssembly。

WebAssembly:一种可在浏览器中运行的、接近原生性能的二进制格式,主要用于执行 计算密集型性能要求高 的任务,解决 JavaScript 在性能方面的瓶颈。

  • 常用 Rust 进行开发,编译为 wasm 文件。
  • 场景:加密算法、机器学习、图像视频处理、数学计算、3D 渲染。

WebSocket

HTTP 是请求响应型协议,适合页面加载和接口调用;

WebSocket 基于 HTTP 1/1,全双工、持久连接协议,适合高频、实时、服务端主动通信场景,如聊天、推送等

WebSocket 连接方式:

  1. 客户端 HTTP 发起 “协议升级” 请求:Connection: Upgrade
  2. 服务器响应并切换协议:HTTP/1.1 101 Switching Protocols + Upgrade: websocket
  3. 握手成功后,简历持久连接,进行双向通信;
    • 前端基于事件驱动,通过监听 + 回调函数实现信息的读取。
特性 / 协议HTTPSSE(Server-Sent Events)WebSocket
通信方向单向:客户端 → 服务端单向:服务端 → 客户端双向:客户端 ↔ 服务端
连接模式每次请求一次连接建立长连接建立一次后保持连接
协议类型标准 HTTPHTTP/1.1 + text/event-streamHTTP 握手升级为 WebSocket
浏览器支持✅ 所有浏览器✅ 大部分浏览器(不支持 IE)✅ 基本支持
传输格式任意格式纯文本(特定格式)文本 + 二进制帧
重连机制无需重连✅ 自动重连(内置)❌ 需手动维护
心跳机制无序心跳服务器定期发送 :ping❌ 需手动维护
适用场景常规直播评论、AI 流等单向推送聊天、实时交互场景

同源策略 / 跨域

同源请求、跨域请求。

同源策略(Same origin policy),它是由 Netscape 网景公司提出的一个著名的安全策略,浏览器都遵守该策略。

  • 同源: 协议、域名、端口号必须完全相同

  • 跨域: 违背同源策略就是跨域,浏览器会 丢弃 非同源的响应数据。

通过 window.originlocation.origin 得到当前源。

http://moxy.com/index.html
http://moxy.com/server.php
//同源

http://a.wang.com
http://wang.com
//不同源,域名必须一模一样
  • 服务端有返回数据,浏览器也接收到了响应数据,但浏览器发现我们请求的是一个非同源的数据,浏览器再将其响应报文丢弃掉。

同源策略又分为以下两种:

  1. DOM 同源策略:禁止对不同源的页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
  2. XMLHttpRequest 同源策略:禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。无需再浏览器收到请求后拦截非同源数据,通过 XHR 发送的不同源请求可直接被拦截。

🍊(2)跨域

当用户在 A 域中访问服务器获取资源,服务器会正常的返回资源。而当用户试图在其他域的网站去访问 A 域的资源,出于安全原因,服务器就会拒绝这种访问方式。

  • 浏览器发送请求时,会把本地域放在请求头中发送给服务器,以便服务器对齐对其进行验证。

可以跨域使用CSSJS和图片

  • 同源策略限制的是数据访问,我们引用 CSSJS 和图片等资源不限制。

同源策略会让三种行为受限:

  • Cookie、LocalStorage 和 IndexDB 访问受限;
  • 无法操作跨域 DOM(常见于 iframe);
  • Javascript 发起的 XHR 和 Fetch 请求受限;

💊 为什么跨域限制:

如果没有 DOM 同源策略,也就是说不同域的 iframe 之间可以相互访问,那么黑客可以这样进行攻击:

  1. 做一个假网站,里面用 iframe 嵌套一个银行网站 http://mybank.com
  2. 把 iframe 宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
  3. 这时如果用户输入账号密码,我们的主网站可以跨域访问到 http://mybank.com 的 dom 节点,就可以拿到用户的账户密码了。

如果没有 XMLHttpRequest 同源策略,那么黑客可以进行 CSRF(跨站请求伪造) 攻击:

  1. 用户登录了自己的银行页面 http://mybank.comhttp://mybank.com 向用户的 cookie 中添加用户标识。
  2. 用户浏览了恶意页面 http://evil.com,执行了页面中的恶意 AJAX 请求代码。
  3. http://evil.comhttp://mybank.com 发起 AJAX HTTP 请求,请求会默认把 http://mybank.com 对应 cookie 也同时发送过去。
  4. 银行页面从发送的 cookie 中提取用户标识,验证用户无误,response 中返回请求数据。此时数据就泄露了。
  5. 而且由于 Ajax 在后台执行,用户无法感知这一过程。

https://juejin.cn/post/6844903681683357710

💊 解决跨域问题

解决跨域问题有三种方式:民间的 JSONP + 官方的 CORS(跨域资源共享)+ 不安全的 iframe。

跨域拒绝:服务器?浏览器

✅ 跨域请求是浏览器拒绝的,不是服务器拒绝的。

  • 浏览器允许用户发出跨域请求的前提(fetch, XMLHttpRequest),是通过了响应头的检查,允许跨域。

    1. 浏览器发出跨域请求(或者有 OPTIONS 的预检请求),服务器返回响应;

    2. 浏览器检查响应头是否包含:Access-Control-Allow-Origin: 当前网页的 origin

    3. 如果有,允许前端 JS 读取响应内容;如果没有,此时客户端响应了数据,但浏览器阻止前端读取响应,抛出跨域错误。

JSONP

JSONP 是 JSON with Padding 的略称,JSONP 是程序员提出的一种跨域解决方案。

  • 在网页有一些标签天生具有跨域能力,比如:imglinkiframescript。发出请求不受跨域限制。

JSONP 通过创建 script 标签,利用 src 属性进行跨域,来发送请求。仅支持 get 请求

缺点:

  1. 代码结构改变,接受到响应数据后的处理代码,要全部放在回调函数中。
  2. 控制反转。调用回调函数,处理返回数据和后续逻辑的契机不在浏览器的 js 代码中,而是交给了服务器。
  3. 后端协商,需要和后端进行协商,确保服务器会正确调用回调函数,并携带正确的格式。JSONP 不易进行错误检查。

CORS

CORS(Cross-Origin Resource Sharing),跨域资源共享。CORS 是官方的跨域解决方案。CORS 需要浏览器和服务器同时支持,支持 get 和 post 请求。

  • 允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。

  • 服务器的响应请求中设置:"Access-Control-Allow-Origin" = *

    实现 CORS 通信需要浏览器和服务器都支持。

CORS 背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息:

  • 通过设置一个响应头来告诉浏览器,该请求允许跨域,而浏览器会对头部信息匹配,如果匹配成功则正常返回信息,浏览器收到该响应以后就会对响应放行,而不会拦截。

**注意:CORS 不支持IE8/9,**如果要在IE8/9使用CORS跨域需要使用XDomainRequest对象来支持CORS

浏览器 CORS

浏览器会限制 从脚本内发起 的跨域 HTTP 请求。 例如 XHR 和 Fetch 就遵循同源策略。这意味着使用 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源。

Web 程序发出跨域请求后,浏览器会 自动 向我们的 HTTP header 添加一个额外的请求头字段:OriginOrigin 标记了请求的站点来源:

GET https://api.website.com/users HTTP/1/1
Origin: https://www.mywebsite.com // <- 浏览器自己加的

服务器返回的 response 也会添加一些响应头字段,这些字段将 显式表明 此服务器是否允许这个跨域请求。

客户端 CORS

服务器开发人员,通过验证 Origin 是否允许跨域访问,然后在 HTTP 响应中添加额外的响应头字段 Access-Control-* 来表明是否允许跨域请求。

all.get("/testAJAX", function (req, res) {
//通过res设置响应头,允许跨域请求
//res.set("Access-Control-Allow-Origin","http://127.0.0.1:3000");
res.set("Access-Control-Allow-Origin", "*");
res.send("testAJAX 返回的响应");
});

iframe

<iframe> 内容迁入技术。是框架的一种形式,一般用来包含别的页面,例如我们可以在我们自己的网站页面加载别人网站或者本站其他页面的内容。

iframe 的核心属性就是 src,表示要引用的页面一个请求,src 实际就是对跨域服务器的一个请求。

iframe 通常要解决不同页面(不同 iframe)的通信问题:

  • document.domain 作用是获取 / 设置当前文档的原始域。

    同源策略会判断两个文档的原始域是否相同来判断是否跨域。这意味着只要把这个值设置成一样就可以解决跨域问题。

  • window.postMessage 方法可以安全地实现跨源通信,写明目标窗口的协议、主机地址或端口就可以发信息给它。

    为了安全,收到信息后要检测下 event.origin 判断是否要收信息的窗口发过来的。

iframe 的缺点:

  1. 页面多。会产生很多页面,不容易管理,多个页面如果有各自的滚动条,影响用户体验。
  2. 请求多。iframe 框架页面会增加服务器的 http 请求。
  3. 手机兼容性。很多的移动设备(PDA 手机)无法完全显示框架,设备兼容性差。
  4. SEO 不友好。无法被一些搜索引擎索引到,搜索引擎爬虫不能处理 iframe 中的内容,不利于搜索引擎优化。
  5. 不安全。容易发生 xss 攻击,钓鱼等攻击方式。比如非法网站,iframe 内嵌一个合法网站,用户输入了账号密码登录后,非法网站就获取了相关数据。

事件流

**(1)三个阶段:**DOM 2 级事件规定,事件的事件流三个阶段:捕获 => 目标 => 冒泡。从 Window 向下到 target,再返回。

  • addEventListener()removeEventListener() 三个参数。
    • 事件名、回调函数、处理阶段:true 捕获,false 冒泡(默认)。
    • 如果要移除事件,对应的三个参数必须完全相同。
  • 停止冒泡:event.stopPropagation()

在 DOM 事件流中, target 在捕获阶段不会接受到事件,所以对 target 绑定 捕获事件 是无效的,默认修改为冒泡。

(2)DOM 0 和 DOM 2 的执行优先级:

  • DOM 0 级事件和 DOM 2 级事件可以共存。
  • DOM 0 级只有冒泡。所以先执行 DOM 2 级的捕获阶段。在冒泡阶段,DOM 0 级和 DOM 2 级按绑定顺序执行,无优先级。

(3)事件委托

  • e.target触发 事件的元素。从该对象中,可以获得触发事件的元素名、id、属性、子节点等要素。

  • e.currentTarget绑定 事件的元素。

  • 应用:多列表绑定、React 合成事件

DOM 0 / 2 事件模型

DOM 0 级:绑定事件使用属性形式:on + 事件名。如 onclick、onmousemove、onmouseover。

  • 如果对同一个元素绑定相同的事件,后边的会覆盖掉前边的。并且 DOM0 级事件只 触发冒泡,不能触发事件捕获阶段 。

DOM 2 级:绑定事件使用函数形式: addEeventListener();删除事件使用 removeEeventListener()

  • DOM 2 级允许对同一个元素绑定多个相同的事件,后面的 不会覆盖 前面的。 使用 addEventListener 方法为一个元素绑定事件时,它 默认冒泡阶段触发。如果第三个参数为 true,在捕获阶段也能触发。

注意:使用 DOM 0 和 DOM 2 同时添加事件模型,两者是不冲突的。

鼠标点击事件

点击鼠标左键,依次触发:MouseDown、MouseUp、Click

双击鼠标左键,依次触发:MouseDown、MouseUp、Click、Dbclick、MouseUp

单机鼠标右键,依次触发:mousedown、contextmenu、click

click:单击鼠标左键时发生,如果右键也按下则不会发生。
- 当用户的焦点在按钮上并按了 Enter 键时,同样会触发这个事件
dblclick:双击鼠标左键时发生,如果右键也按下则不会发生
mousedown:单击任意一个鼠标按钮时发生
mouseup:松开任意一个鼠标按钮时发生
mousemove:鼠标在某个元素上时持续发生
contextmenu:鼠标按下右键触发,并出现上下文菜单

mouseout:鼠标移动到盒子外触发(子元素算边界、冒泡)
mouseover:鼠标移动到盒子内触发(子元素算边界、冒泡)
mouseleave:鼠标移动到盒子外触发(不冒泡)
mouseenter:鼠标移动到盒子内触发(不冒泡)

// event.button:获取鼠标按下的键 click、mousedown、mouseup事件类型
左键:0
中键:1
右键:2

假设:container 盒子内嵌套 box 盒子,在 container 上绑定 mouseover、mouseout、mouseleave。

  • 当鼠标从 container 移入内部的 box 时,会触发 mouseover 和 mouseout,但不会触发 mouseleave。
  • mouseleave 只有在真正离开盒子才会触发。
  • mouseover,代表鼠标来自哪个元素(包括子元素)。
  • mouseout:代表鼠标去往哪个元素(包括子元素)。

判断两个 dom 节点相同

  • 使用 === 来比较两个元素
  • A.isSameNode(B):是同一个节点时返回 true。DOM 4 被废弃。
  • isEqualNode():检查两个节点是否相等,不一定相同。

浏览器存储 Storage、IndexedDB、Cookie

(1)🌈 Storage

WebStorage 主要提供了一种机制,可以让浏览器提供一种比 cookie 更直观的 key / value 存储方式:

  • localStorage:本地存储,永久性存储。网页关闭后,存储的内容依然保留;
  • sessionStorage:会话存储,本次会话的存储。在浏览器中一个标签是一个会话,关闭掉会话后,清除存储;

localStorage 和 sessionStorage 都具有以下方法:

属性 1:Storage.length:只读。 存储在 Storage 对象中的数据项数量;

方法 5:获取 key、写入 key / value、删除 key、清空。

  • Storage.key(index):获取第 index 个成员的 Key ;
  • Storage.getItem(key):获取 key 对应的 value;
  • Storage.setItem(key, value):把 key / value 添加到存储中;
    • 如果 key 已经存在,则更新值;
  • Storage.removeItem(key):删除成员;
  • Storage.clear():清空 storage。

(2)🌈IndexedDB

DB:Database 数据库。

  • 在服务器端比较常见。实际开发中,大量的数据都是存储在数据库的,客户端主要是请求这些数据并且展示。
  • Storage:(常用)存储简单的数据到本地(浏览器中),如 token、用户名、密码、用户信息等。
  • IndexedDB:(少用)大量的数据需要存储。

IndexedDB 是一种底层的 API。用于在客户端存储大量的结构化数据。

  • 一种事务型数据库系统,基于 JavaScript 面向对象数据库,类似于 NoSQL(非关系型数据库)。
  • IndexDB 本身基于事务,程序员只需指定数据库模式,打开与数据库的连接,检索、更新一系列事务即可。
    • 事务:一个操作单元。涉及:事务隔离、事务回滚、事务传播等。
  • 数据库的增删改查,效率比 Storage 效率更高。

(3)🌈Cookie

Cookies:文本,通常是网站为了辨别用户身份而存储在用户本地终端 (Client Side)上的数据。

  • 客户端可以通过 setCookie 添加 cookie 到客户端。
  • 浏览器发送请求时,可以把 cookie 携带发送;

Cookie 总是保存在客户端中。按在客户端中的存储位置,Cookie 可以分为内存 Cookie 和硬盘 Cookie。

  • 内存 Cookie 由浏览器维护,保存在内存中,浏览器关闭时 Cookie 就会消失,其存在时间是短暂的;
  • 硬盘 Cookie 保存在硬盘中,有一个过期时间,用户手动清理或者过期时间到时,才会被清理;
    • 没有设置过期时间,默认是内存 cookie。
// http 请求头中
Set-Cookie: "name=why; path=/; expires=Wed, 10 Nov 2021 09:35:46 GMT; httponly"
// 保存了:Key/Value 内容、保存路径、过期时间等
  • 如果响应中添加了 cookie,浏览器自动读取并保存 cookie,也会再下次往同一服务器发送请求时,携带 cookie。

Cookie 的属性:

  • 生命期:Expires 具体时间 、Max-Age 生命秒数,下文有;
  • 作用域:为文档来源 + 文档路径:Domain 包含的域名、Path 包含的路径,下文有;
    • 文档来源(domain 属性):同 localStorage 效果。
    • 文档路径(path 属性):默认情况下, cookie 的可访问权限为 该网页位于相同目录和子目录下的其他网页(即,兄弟网页、子网页、兄弟的子网页)。
      • 也可以指定 path 路径。这样来自同一服务器的任何网页,都可以访问一个 cookie。
  • 缺点:
    • 客户端每次请求,都会携带 cookie,有可能不需要,所以造成性能浪费。
    • 有体积限制:4KB。
    • 明文传输,安全性较低。
  • 安全:
    • Secure:只能使用 HTTPS 安全协议时,才传输 cookie。
    • HTTPOnly:禁止客户端修改 cookie,防止 XSS 攻击(document.cookie 修改 cookie)。
    • SameSite:让 Cookie 在跨站请求时不会被发送,阻止跨站请求伪造攻击(CSRF)。
    • Strict:完全禁止第三方(跨站)Cookie,sso 登录等功能将失效。
    • Lax:允许部分第三方请求携带 Cookie,如:a、link、get。不允许:post、iframe、ajax、图片。
    • None:不限制。跨站可以携带 cookie。

缓存区别

特性/维度IndexedDBlocalStorage / sessionStorageCookie
数据结构键值对,支持对象存储、索引键值对(必须是字符串)键值对(必须是字符串)
数据容量限制较大(几十 MB ~ 几百 MB)较小(约 5MB)很小(约 4KB)
同步 or 异步✅ 异步 API❌ 同步(阻塞主线程)❌ 同步(阻塞主线程)
是否自动带到请求头❌ 否❌ 否✅ 是(自动随 HTTP 发送)
可否跨域访问❌ 否(受同源限制)❌ 否❌ 否
持久性长期(除非主动删除)localStorage:持久 sessionStorage:关闭页面即清除通常会设置过期时间
类型限制支持复杂对象只能字符串只能字符串
是否支持事务/查询✅ 支持事务、索引、查询❌ 不支持❌ 不支持
是否安全(敏感信息)✅ 不随请求发出,JS 端访问✅ 不随请求发出❌ 易泄漏,需加 HttpOnly/Secure
是否适合长期存储✅ 是✅ localStorage 是,❌ sessionStorage 标签关闭后删除❌ 过期或被服务端覆盖

相同:

  1. 存储空间均为 5101k,约等于 4.98M。
  2. 存储的格式为 string。

不同:生命期 + 作用域。

生命期:

  • localStorage:数据是永久性的,除非 Web 应用或用户手动删除。

  • sessionStorage :数据的生命期和标签页相同。标签页关闭后,数据会被删除。

  • cookie

    • expires:设置的是 Date.toUTCString(),具体的某个时间。
    • max-age:设置过期的秒钟,max-age=max-age-in-seconds (例如一年为60*60*24*365);

作用域:

  • localStorage:作用域为 文档来源,由 协议、域名、端口 共同定义。同源文档共享 localstorage 数据。

  • sessionStorage :作用域为 文档来源 + 窗口隔离 。用户在两个标签页中打开了同一来源的文档,这两个标签页的 sessionStorage数据也是隔离的。

  • cookie:(允许 cookie 发送给哪些 URL)

    • Domain:指定哪些主机可以接受 cookie。cookie 允许 同站 发送(顶级 + 二级域名相同 kuai.com

      • 如果不指定,那么默认是 origin 原始域名,不包括子域名。
      • 如果指定,则包含子域名。
      • 例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中,如 developer.mozilla.org
    • Path:指定浏览器在哪些 URL 路径可以携带 cookie 发送请求。

      • 例如,设置 Path=/docs,则以下地址都会匹配: /docs , /docs/Web/ , /docs/Web/HTTP

使用场景

localStorage

  • 存储用户访问习惯:登录次数、登录时间等
  • 用户本地化配置:用户语言、夜间主题等

sessionStorage

  • 单页面应用(SPA)路由跳转后,希望在当前会话内保持某些页面状态:当前页的滚动位置、某个操作步骤是否完成;
  • 防止重复提交 / 防止页面重复操作:某个表单已经提交,即使用户刷新 + 重复点击,也不会重复提交;
  • 输入的表单数据,临时保存;

Cookie

  • SSO 单点登录,保存 Ticket。
  • 用户保持登录状态的 Token。

浏览器缓存

DNS 缓存

DNS 定义:Domain Name System 域名系统。万维网上作为域名和 IP 地址相互映射的数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的 IP 数串。DNS 协议运行在 UDP 协议之上,使用端口号 53。

DNS 查询过程如下:

  1. 浏览器 DNS 缓存:如果存在,则解析完成。
  2. 操作系统的 hosts 文件:查看是否存在对应的映射关系,如果存在,则解析完成。
  3. 本地 DNS 服务器:(ISP 服务器,或者自己手动设置的 DNS 服务器),如果存在,则解析完成。
  4. 根服务器:发出请求,递归查询。

11951666519595_.pic[30]

CDN 缓存

CDN 定义:Content Delivery Network 内容分发网络火车票代售点,菜鸟驿站,解决最后 1km。CDN 帮助缓存服务器在最近的 CDN 节点 / 用最短的请求时间拿到资源。同时起到请求分流的作用,减轻服务器负载压力。

在浏览器本地缓存失效后,浏览器会向 CDN 边缘节点发起请求。

类似浏览器缓存,CDN 边缘节点也存在着一套缓存机制。一般都遵循 http 标准协议,和浏览器近似通过响应头中的 Cache-control 字段,来设置 CDN 强缓存的时间和策略。

  • 浏览器协商缓存:浏览器向 CDN 节点发起请求,CDN 节点判断本地缓存数据是否过期:
    • 没过期,则和浏览器按协商规则返回资源;
    • 过期,则 CDN 向服务器更新最新数据,然后再和浏览器按协商规则返回资源。
  • CDN 服务商会基于文件后缀、目录多个维度来指定 CDN 缓存时间,精细化缓存管理。

CDN 优势:

  1. CDN 节点解决了跨运营商和跨地域访问的问题,访问延时大大降低。
  2. 大部分请求在 CDN 边缘节点完成,起到了分流作用,减轻了源服务器的负载。

强缓存 / 协商缓存

浏览器缓存的意义:设置缓存后,就能将第一次访问到的资源存在本地,当第二次再去访问时就可以直接访问本地的资源,也就不用去服务器上拉取。

(略) 浏览器缓存的位置:2+2

  • Memory Cache:基于内存的缓存,读取高效速度快,关闭网页,内存释放。

  • Disk Cache:基于磁盘的缓存,容量大,读取慢,存储在本地中。

  • Service Worker:浏览器额外的独立线程,它可以控制缓存文件、匹配方式、读取缓存,存储在磁盘中。

  • Push Cache:HTTP/2,不设置缓存策略时,采用启发式算法(下问),缓存在 sessionStorage 内存中。

缓存策略:3

通过设置 HTTP Header 来实现缓存策略:强缓存协商缓存不设置缓存策略

截屏2022-08-22 21.48.16

  • 强缓存:不向服务器发送请求,直接从缓存中读取资源,两种 HTTP Header 实现:

    • Expires:HTTP/1 属性。缓存过期时间,指定资源到期时间。是服务器端具体的时间点,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

    • Cache-Control: HTTP/1.1 属性,优先级高于 Expires。

      • max-age:强缓存。缓存最大过期时间(秒);如果设置成为 0,则为协商缓存。
      • no-cache:协商缓存。本地有缓存资源,每次都向服务端请求校验:获取新资源 200/ 使用本地缓存 304。
      • no-store:不缓存。禁止本地缓存资源,每次请求都必须从服务器获取资源。
  • 协商缓存:强制缓存的内容失效后,浏览器携带缓存标识向服务器发起请求,服务器根据缓存标识,判断客户端存储的本地资源是否已经过期,通知客户端是否继续使用本地缓存。一致则返回 304,否则返回 200 和最新的资源。

    • 两种 HTTP Header 实现:Last-ModifiedETag

    • Last-Modified:HTTP/1,资源的最后修改时间。

      • 第一次访问资源:服务器在 response 头里添加 Last-Modified 服务器最后修改文件的时间点。
      • 第二次访问资源:检测到缓存文件里有 Last-Modified,在请求头里加 If-Modified-Since,值为 Last-Modified 的值。服务器拿这个值和请求文件的最后修改时间作对比:没有变化,返回 304;如果小于最后修改时间,说明文件有更新,就会返回新的资源,状态码为 200。
    • ETag: HTTP/1.1,资源的唯一标识。

      • 初次请求:服务器返回一个新的 Etag: token
      • 二次请求:把 token 包裹在请求头里的 If-None-Match 发送给服务器。服务器比较新旧 token:一致,返回 304 通知浏览器使用本地缓存;不一致,返回新资源,新 ETag,状态码为 200。
    • 对比:ETag 更好。精确 ,Last-Modified 只能精确到秒级、若资源重复生成内容不变,则 Etag 值不变。

  • 不设置缓存策略:浏览器采用启发式算法,缓存时间 = (取响应头中的Date - Last-Modified) * 10%

  • https://juejin.cn/post/7061588533214969892#heading-52

🍊 资源刷新

前端工程打包后的典型结构

  • 一个 HTML 文件(比如 index.html)
  • 若干个 CSS 文件(可能是多个 chunk 或合并后的主 CSS)
  • 若干个 JS 文件(通常是多个 chunk 或合并后的主 JS)
  • 图片、字体等静态资源

打包后,index.html 文件中通过

上线后问题:客户端缓存了老版本的 JS/CSS,导致用户看到的不是最新版本,或页面出错。

  • 比如你部署了新的代码,但用户的浏览器还加载了旧的 main.js,而这个文件已经变了,造成资源不匹配的问题。

分析流程:

(1)HTML 模板

HTML 模板地址是不会变化的,所以首先需要确定客户端 “是否重新请求模板”:

  • HTTP 头配置:
缓存策略客户端行为
Cache-Control: no-cache + ETag, max-age=0有缓存,每次请求时会向服务器验证是否有新版本;✅ 协商缓存,触发请求:CDN 返回 200 或 304
Cache-Control: max-age=31536000, immutable完全不发请求(浏览器直接用本地副本);❌ 强缓存,不请求,仍旧使用本地缓存
Cache-Control: no-store浏览器不缓存,每次都请求;❌ 每次都会获取最新版本,但性能较差
  • 所以,html 模版通常会定义为 max-age=0,客户端每次打开时加载最新模板。

(2)css、js 文件

js 文件有三种情况:

  1. SDK js 文件,名称固定,不带 hash 后缀;
    1. cdn 刷新:上线后刷新 CDN,确保客户端再次请求时,拉取最新代码;
      • 请求头:需要和 html 文件一样,配置协商缓存;
      • 可以做灰度、热更新、A/B 实验时使用;
    2. 内部动态更新:js 文件前置接口请求,拉取最新的代码;
      • 请求头:无需配置协商缓存,sdk 代码可固定。
  2. 增加 hash 后缀(main.123.js);
    • 可以前端配置:vite/webpack 打包工具增加 hash;
    • 可以后端配置:后端动态模板 .ejs 在上线时,对 main.js 拼接成 main.{时间戳}.js
    • 请求头:可以强缓存,无需协商缓存;
  3. 增加 query 后缀(main.js?v=123);
    • 通常是后端配置,模板系统增加:main.js?v=${buildTime},便于版本控制;
    • CDN 需要增加 query 参数区分。不同 query 的同一个路径,需要识别为不同资源;
    • 服务器端需要返回协商缓存头。使用强缓存浏览器不会更新;
项目开发接入复杂度运维复杂度缓存推荐程度
固定名称 + CDN 刷新前后端无需配置需刷 CDN,需协商缓存协商缓存,max-age=0SDK、HTML 常用
hash 后缀前端可配,后端可配无需操作强缓存、协商缓存均可✅ 最常用
query 后缀后端可配需协商缓存协商缓存,max-age=0旧项目常用

性能

重排、重绘、合成

名称:重排(Reflow 回流)、重绘(Repaint)

重排重绘和事件循环:

  • 浏览器 60Hz 刷新率,每 16ms 会更新一次页面。当 Event loop 执行完 Microtasks 后,会判断 document 是否需要更新。
  • 然后会判断是否有 resize 窗口、 scroll 滚动监听。这两个事件间隔 16ms 才触发一次,自带节流功能

🌈 1. 更新了元素的几何属性(重排)

div.style.height = xxx

通过 JavaScript 或者 CSS 修改元素的 几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发 重新布局,解析之后的一系列子阶段,这个过程就叫 重排

总结:重排需要更新完整的渲染流水线,开销最大,重排会触发重绘。

🌈 2. 更新元素的绘制属性(重绘)

div.style.background = xxx

如果修改 没有 导致元素 几何位置的变换 ,比如更新元素的背景颜色,那么 布局阶段 将不会被执行,直接进入了 绘制阶段,然后执行之后的一系列子阶段,这个过程就叫 重绘

总结:重绘省去了布局和分层阶段,执行效率比重排高一些。

🌈 3. 避开重排和重绘(合成)

transform: translate(xxx, xxx)

如果既不修改元素的几何位置属性,也不修改元素的背景色等属性,渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做 合成

比如,使用 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。

总结:相对于重绘和重排,合成能大大提升绘制效率。

减少重排、重绘

导致重排(回流)的操作:

  • 改变元素尺寸、内容 (文字、图片)、字体(大小、风格)。
    • 激活 CSS 伪类状态(例如 :hover
  • 改变 DOM 树结构:添加、移动、删除、隐藏(display: none) DOM 节点。
    • 改变浏览器窗口
  • 获取一些特定属性的值
    • offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight
    • getComputedStyle 获取元素的 styled 属性。
    • 即时性、准确性。这些属性通过即时计算得到。浏览器为了获取这些值,会进行回流。

减少重排重绘

  1. **减少重排的范围。**减去不需要重排的元素。
  2. **读写分离操作。**对 DOM 属性要读写分离,因为读的前提是要绘制出来,读之前要触发绘制。批量写入后再读取,减少了重绘次数。
  3. **样式集中改变。**通过对 class 名操作样式,而不是频繁操作 style。利用 class 集中改变样式。
  4. **批量 dom 操作。**例如 createDocumentFragment,或者使用框架,例如 React
    • 将 DOM 从树上摘下(display: none) 后,批量修改再放上。
  5. **限制窗口大小的调整。**窗口大小变化,就一定会导致重排重绘。
  6. 采用合成手段,使用 transform,避开重排和重绘阶段。

前端优化策略

  1. 建立监控体系

    1. 利用现成(阿里云 ARMS、听云、监控宝)或自行搭建埋点监测。
  2. 确定采集指标

    • JS Error:解析后可以细分为运行时异常、以及静态资源异常。

    • 请求异常:采集 ajax 请求异常。

    • DNS, TCP, DOM 解析等阶段的指标。

    • 首次绘制时间( FP ) :即 First Paint,为首次渲染的时间点。

      首次内容绘制时间( FCP ) :即 First Contentful Paint,为首次有内容渲染的时间点。

      最大内容绘制 (LCP):即 Largest Contentful Paint,测量加载性能。为了提供良好的用户体验,LCP 应在页面首次开始加载后的2.5 秒内发生。

      首次交互时间(FID):即 First Input Delay,记录页面加载阶段,用户首次交互操作的延时时间。FID 指标影响用户对页面交互性和响应性的第一印象。

      累积布局偏移 (CLS):即 Cumulative Layout Shift,测量视觉稳定性。为了提供良好的用户体验,页面的 CLS 应保持在 0.1 或更少。

  3. 进行优化

(1)资源优化:

  • 文本优化:Brotli + Gzip 纯文本压缩:HTML、CSS、SVG、JavaScript 等。
  • 图像优化:
    • 使用响应式图像和 WebP。与 pngjpg 相比,相同的视觉体验下,WebP 图像的尺寸缩小了大约 30%。缺点:不支持 JPEG 的渐进式渲染,先模糊,后清晰。
    • 图像懒加载,率先加载用户视口出现的图像。
    • 渐进加载图像,先模糊,后清晰。
    • 瀑布流,图片加载完一个,就通过动画添加到瀑布流中。类似 pintrest。
    • 骨架屏,图片不一定要立即加载,可以先加载 dom,再加载耗时资源。
    • 分批加载,主动将对图片的网络请求分批次异步加载,防止耗时过长,掉帧卡顿。
    • 按需加载。按照屏幕分辨率加载图像质量,用户点击图像才加载更高清图像。
    • JPEG、png、SVG 等都有对应的压缩协议。
  • 字体优化:
    • 字体包通常不需要适配所有的文字,可以对字体进行子集化 subfont。
    • 使用 preload 来预加载字体。

(2)构建优化

  • tree-shaking :清理构建包中无用依赖。让构建结果只包含生产中实际使用的代码。借助 Webpack 可以检测到 import 链可以在哪个位置终止并转换为一个内联函数,而不破坏代码。

  • code-spliting:组件懒加载。把代码拆分为按需加载的chunk。并不是所有 JavaScript 都必须立即下载、解析和编译。一旦在代码中定义了分割点,Webpack 就可以处理依赖关系和输出文件。它可以让浏览器保持较小的初始下载量,并在应用程序请求时按需请求代码。

  • preload-webpack-plugin:该插件根据代码的分隔方式,引导浏览器使用 <link rel="preload"><link rel="prefetch"> 对分隔的代码 chunk 进行预加载。

  • 识别并删除未使用的 CSS / JS。Chrome 中的 CSS 和 JavaScript 代码覆盖率工具(Coverage) 可以让我们了解哪些代码已执行或应用,哪些未执行。我们可以启动一个覆盖率检查,然后查看覆盖率结果。一旦检测到未使用的代码,找出那些模块并使用 import() 延迟加载

    • mock 单元检查,将用不到的函数和逻辑检查出来,然后删除。
  • 设置 HTTP 缓存报文头

    • 检查 expiresmax-agecache-control 和其他 HTTP 缓存报文头是否已正确设置。一般来说,资源可以在 很短的时间内或无限期 缓存,并且可以在需要时通过 URL 中更改其版本。确保没有发送不必要的报头。
  • 避免回流和重绘🔗

    • 减少重排的范围。减去不需要重排的元素。
    • 读写分离操作。对 DOM 属性要读写分离,因为读的前提是要绘制出来,读之前要触发绘制。批量写入后再读取,减少了重绘次数。
    • 样式集中改变。通过对 class 名操作样式,而不是频繁操作 style。利用 class 集中改变样式。
    • 批量 dom 操作。例如 createDocumentFragment,或者使用框架,例如 React
      • 将 DOM 从树上摘下(display: none) 后,批量修改再放上。
    • 避免使用 table 布局。table 通常会整体发生改变。
    • 优化动画。动画的平滑效果与对 CPU 资源的消耗要达到平衡,还可以启动 GPU 加速。
    • 限制窗口大小的调整。窗口大小变化,就一定会导致重排重绘。
    • 采用合成手段,使用 transform,避开重排和重绘阶段。
  • 服务端渲染

(3)传输优化

  • JavaScript 异步加载。defer 对 script 异步加载,并顺序执行不阻塞 dom。
  • 关键 CSS。找到影响首屏的 CSS 样式规则,并拆分后率先加载。
  • 加快请求速度:预解析 DNS;使用 HTTP2.0,并行加载; 使用 CDN 分发
  • 减少事件委托、增加防抖节流、HTTP1.0 使用精灵图减少请求次数。CSS 写头、JS 写底。

节流、防抖

应用背景:当某个事件频繁触发,就会导致频繁的调用回调函数,造成性能损失。防抖和节流的作用,就是降低事件触发的频率,在一个周期内只触发一次即可。

(1)节流(throttle):事件触发时,就会立即执行。

  • 如果事件被频繁触发,节流函数会按照 固定频率 来执行函数。

应用场景

  • 监听页面的滚动事件;
  • 监听鼠标移动事件;
  • 用户频繁点击按钮操作;
  • 游戏中的一些设计:经典飞机大战,用户会持续按下 / 频繁发射按钮,但子弹的射速需要保持一定。

(2)防抖(debounce):只有等待一段时间没有事件触发,才会真正的执行响应函数。

  • 当事件触发时,相应的函数并不会立即触发,会等待一定的时间,延迟执行
  • 当事件密集触发时,函数的触发会被 频繁的推迟

应用场景:

  • 监听浏览器滚动事件,完成某些特定操作;
  • 用户缩放浏览器的 resize 事件。
    • 只在缩放结束,才触发事件,减少页面的频繁渲染,造成卡顿。和 Windows 页面的缩放 / 拖动优化相同
  • 输入框中频繁的输入内容,搜索或者提交信息;
    • 比如,在淘宝中搜索:MacBook。当用户按下 M,会进行一次联想内容搜索,将 M 开头的内容呈现出来。但用户可能对 M 开头的内容联想并不关心。依次输入剩余的字母。如果没有防抖,会进行 7 次网络请求,消耗系统性能。
      • 引入防抖:设置 1s 的防抖,用户如果快速输入 "Mac",在期间时不会进行网络请求的联想搜索的。只在 'Mac' 输入结束时,进行一次网络请求。
        • 从用户角度:用户可能会对 Mac 的相关产品感兴趣,有可能忘记 MacBook 的全称怎么拼。
        • 从性能角度:节约了多次网络请求。

节流(throttle)- 时间戳版

function throttle(func, delay) {
var last = 0;
return function () {
var now = Date.now();
if (now >= delay + last) {
func.apply(this, arguments);
last = now;
} else {
console.log("距离上次调用的时间差不满足要求哦");
}
};
}

function resize(e) {
console.log("窗口大小改变了");
}
window.addEventListener("resize", throttle(resize, 1000));

节流(throttle)- 定时器版

function throttle(func, delay) {
var timer = null;
return function () {
if (!timer) {
func.apply(this, arguments);
timer = setTimeout(() => {
timer = null;
}, delay);
} else {
console.log("上一个定时器尚未完成");
}
};
}

function resize(e) {
console.log("窗口大小改变了");
}
window.addEventListener("resize", throttle(resize, 1000));

防抖(debounce)

function debounce(func, delay) {
var timeout;
return function () {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}

function resize(e) {
console.log("窗口大小改变了");
}
window.addEventListener("resize", debounce(resize, 1000));

其他

页面间通信

同源:Local Storage

Local Storage 用于存储数据,通过 storage 事件,可以对存储状态进行监听,从而达到页面间通信的目标。

// A页面
window.onstorage = function (e) {
console.log(e.newValue); // previous value at e.oldValue
};
// B页面
localStorage.setItem("key", "value");

同源:indexDB

  • 用于客户端存储大量结构化数据,检索性能优秀,区别于 LocalStorage只能存储字符串,IndexedDB可以存储 JS所有的数据类型,包括 nullundefined等,是 HTML5规范里新出现的 API。

同源:BroadcastChannel

  • 兼容性差的 API。A 页面广播信号,创建自身引用。B 页面舰艇同源页面下的广播信号,并通过引用名,判断广播源,来分辨信息。

跨域:postMessage

  • A 页面通过 window.open 获得 B页面的引用,向 B页面发送信号,并监听 B页面回传回来的信号。
// A
var opener = window.open("http://127.0.0.1:9001/b.html");
opener.postMessage("red", "http://127.0.0.1:9001");

// B
window.addEventListener("message", (event) => {
// origin属性判断消息来源地址
if (event.origin === "http://127.0.0.1:9001")
cosole.log(event.data);
}

跨域:webSocket

  • WebSocketHTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议,常用的场景是即时通讯。webpack 的热更新就是利用该原理。