关于前端:ETag-接口软缓存

39次阅读

共计 3947 个字符,预计需要花费 10 分钟才能阅读完成。

在线测试 Demo 成果比照

千条数据,657k 响应体,size 缩小 99%,接口耗时缩短 94%。
  • get 参数携带在 url 中有长度限度,仅以 post 申请作为测试示例
  • 因为作者以后环境上行带宽大,而作者服务器应用最低配置的网络,上行带宽小。故可根本疏忽上行耗时。
  • size 由优化前的 657000B 升高为 310B,缩小 656690B,缩小 99% 体积
  • Time 由优化前的 3460ms 升高为 193ms,缩小 3267ms,缩小 94% 耗时(留神,这个是因为作者的上行带宽小,商用带宽不会有这么大的差别)

百条数据,65.9k 响应体,size 缩小 99%,接口耗时缩短 70%。
  • size 由优化前的 65900B 升高为 310B,缩小 65590B,缩小 99% 体积
  • Time 由优化前的 68ms 升高为 20ms,缩小 48ms,缩小 70% 耗时

十条数据,7k 响应体,数据量较低,网速影响较大
  • size 由优化前的 7000B 升高为 310B,缩小 6690B,缩小 95% 体积
  • Time 由优化前的 86ms 升高为 41ms,缩小 45ms,缩小 52% 耗时

一条数据,1.2k 响应体,数据量更低存在肯定误差
  • 接口返回 size 由 1200B 降为 310B,缩小 1200-310=890B,缩小 890 / 1200 = 74% 体积
  • Time 由优化前的 32ms 升高为 23ms,缩小 9ms,缩小 28% 耗时

Demo 介绍

  • 运行逻辑

    • 先填写申请参数(仅反对对象构造),服务端会把申请参数将会作为响应体返回给浏览器。在右侧栏中显示返回后果。
    • 以此来定制模仿特定体积响应体,测试数据量下的成果。
    • 测试前,强制刷新以后页面,以革除浏览器缓存。并勾销控制台的「Disable cache」勾选
  • 性能介绍

    • sendGet,发送 get 申请,当产生两次雷同的申请时,第二次会仅返回 304 状态码,size 及 time 比照上一次有明确变动
    • sendPost,发送 post 申请,因为 If-None-Match 自身不会在 get 申请中增加,须要前端自行保护,具体实现逻辑下文将介绍到
    • 测试数据量,可动静生产申请参数,疾速模仿大数据量的申请状况,最大值反对 10000 条数据。

原理

  • 协商缓存 304

    客户端向服务端发动 http 申请时,携带上次申请后果的特征值,服务端比照运行后果与申请中的特征值是否统一,如果特征值统一,则间接返回 304 给客户端,告知文件未扭转,客户端应用本地缓存.

  • 本次优化应用到 http1.1 新增的响应头 ETag,及新增的申请头 f-None-Match 实现协商缓存。

  • 留神:chrome 有个 bug(不晓得是 bug 还是预计设计),get 申请 304 状态在控制台中会被转化为 200,但仍能够通过 network 的 size 列体现缓存的成果

    • https://stackoverflow.com/questions/67017167/why-chrome-dev-tool-shows-a-200-status-code-instead-of-304
    • https://bugs.chromium.org/p/chromium/issues/detail?id=1269602

实现思路

后端,以 node 为例

  • 增加一个中间件,在 response 中做一个全局解决,将响应体做一次 hash,并携带在 ETag 响应头中
  • 若申请头中存在 if-none-match 申请头,则与 hash 值进行比照,统一则返回 304,无需返回响应体

    // Etag 解决的中间层
    function responseWithEtag(request, response, responseData) {const etag = crypto.createHash('md5').update(responseData).digest('hex') // 获取返回参数的 hash 值
      response.setHeader("ETag", etag); // 设置 ETag,标识本次响应信息的 hash 信息,以比照信息是否
      if (request.headers['if-none-match'] === etag) { // 若本次返回值的 hash 信息,与申请头的 if-none-match hash 值统一,则间接返回 304,复用浏览器缓存的数据,无须要额定返回数据,节俭带宽
          console.log('ETag hash match')
          response.writeHead(304, "Not Modified");
          response.end();
          return
      } else { // 无 if-none-match 申请头,或者返回值内容发生变化,hash 匹配不上
          console.log('ETag hash mismatching')
          response.writeHead(200, { "Content-Type": "text/plain; charset=utf-8"});
          response.end(responseData); // 返回业务数据
      }
    }

    前端

  • get 的 ETag 协商缓存已由浏览器中实现,前端无需额定解决
  • post 申请自身不反对 ETag 缓存(post 申请在设计时专为非幂等接口,但实际上曾经很多用于简单查问接口),须要前端自行实现缓存机制

    • 在 post 调用实现后,以申请门路及申请体作为 hash 参数,惟一标识该申请。存储对应的 ETag 响应头,及对应的返回值。
    • 留神,js 获取 ETag 申请头,须要后端凋谢该申请头的拜访权,response.setHeader("Access-Control-Expose-Headers", "ETag"); // 容许裸露 ETag 响应头,否则前端无奈用 JS 获取 ETag 头
    • 在发动 post 申请时,查问该申请是否发动过(以申请门路及申请体作为 hash 参数惟一标识),若发动过,则在申请头中自行添加 if-none-match 申请头
  // 因为 post 申请自身不反对 ETag 缓存,须要前端自行实现缓存机制
  const eTagPostMap = {}
  function ajax(url, type, reqData) {return new Promise((resolve, reject) => {
      let postReqHash = ''; // 以申请门路及其参数的 hash 作为 key 进行缓存
      if (type === 'POST') {postReqHash = md5.create().update(url + JSON.stringify(reqData)).digest('hex'); // 以申请门路及其参数的 hash 作为 key 进行缓存
      }
      const xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function () {if (xhr.readyState === 4) {let { status, responseText, responseXML} = xhr;

          // 留神,GET 申请的 304 响应头在 chrome 承受时,给到 JS 会变成 200 状态码,且有失常的 responseText
          if (status >= 200 && status < 300) {
            // POST 申请的响应头有 ETag,则缓存该后果
            if (type === 'POST' && xhr.getResponseHeader('ETag')) {eTagPostMap[postReqHash] = {
                content: responseText,
                etag: xhr.getResponseHeader('ETag'),
              }
            }
            return resolve(responseText);
          }

          if (status === 304) { // 留神,GET 申请的 304 响应头在 chrome 承受时,给到 JS 会变成 200 状态码
            if (type === 'POST') {return resolve(eTagPostMap[postReqHash].content);
            } else if (type === 'GET') { // 在 chrome 浏览器 GET 申请不会有 304 状态码,在这里只是兜底,以防其余浏览器体现不统一
              return resolve(responseText);
            }
          }
          if (!status) {alert('get 申请不反对这么大的申请参数,请缩小数量 或 应用 post 申请')
          }

          reject(status);
        }
      };

      if (type === 'GET') {xhr.open('GET', url + '?' + serializeParams(reqData), true);
        xhr.send(null);
      } else if (type === 'POST') {xhr.open('POST', url, true);
        // 如果之前发送过这个申请,且有 ETag hash 记录,则当再次发动时,携带上该 hash
        if (eTagPostMap[postReqHash]) {xhr.setRequestHeader("if-none-match", eTagPostMap[postReqHash].etag);
        }
        xhr.send(JSON.stringify(reqData));
      }
    })

hash 性能

  • 因引入了申请体 hash 运算逻辑,需评估其带来的运行耗时
  • 以 1000 条数据 657k,运行 hash 100 次为例,共耗时 180ms,均匀单次耗时 1.8ms
  • 比照缩小的 3267ms 上行耗时,可忽略不计

危险管控

  • 实用场景

    • get,post 幂等的查问类接口,不利用于非幂等接口
  • 疾速切换与回滚

    • 后端下发 ETag 响应头客户端才会应用缓存,整个缓存开关的管制全均在后端,若性能呈现问题,可实现疾速切换复原。
  • 浏览器兼容性反对

    • 局部魔改的浏览器不的确是否反对 http1.1 协定
    • 后端可申请头中的 User-Agent 判断以后客户端浏览器,先从 chrome 反对开始,再逐渐反对其余浏览器,或全量反对。
  • 定制特定接口不缓存

    • 特定接口不下发 ETag 即可敞开该接口的缓存性能,实现接口缓存的高度定制

正文完
 0