跳过正文
  1. 前端工程体验优化实战/

CDN最佳实践:验证、量化与评估

hujiacheng
作者
hujiacheng
Front-end Developer / Strive To Become Better
目录
资源加载优化 - 这篇文章属于一个选集。
§ 3: 本文

CDN是前端应用的核心基础设施,相关改动应该充分验证,确保稳定性,其优化效果也需要专门指标来量化分析,这一节我们就将一起深入探究验证、量化和评估CDN最佳实践的具体细节。

3. 验证,量化与评估
#

1. 上线前验证
#

1. 验证CDN服务器地理位置和HTTP协议版本的影响
#

地理位置和HTTP协议版本的变化,对CDN的HTTP连接耗时会有显著影响,可以通过观察CDN静态资源加载时,Devtool Network标签页测量出的初始化连接、SSL握手等计时数据,对比优化前后耗时,来验证优化效果。

image.png

注:Google介绍QUIC协议(HTTP/3)的论文有提到,应用QUIC协议后,谷歌搜索加载搜索结果页面的指标 Search Latency 有16.7%的改善:

image.png

2. 验证CDN缓存时间配置的影响
#

CDN优化是否生效,可以在上线前通过模拟HTTP请求头、响应头在本地验证。

Chrome Devtool 自113版本开始,新增了覆盖HTTP响应体、响应头(Override web content and HTTP response headers locally)功能,可以帮助我们简单方便地验证CDN缓存时间配置的影响。

具体用法是,在Devtool的Network标签页中选中目标请求,找到对应响应头,鼠标悬浮,就会出现铅笔图标,点击后就可以修改为任意值,并且会在页面刷新后仍然保持生效。

image.png

如果浏览器Devtool不能满足需求,也可以考虑使用WhistleCharles Proxy等能力更全面的专用代理工具,模拟CDN优化后的状态来验证。

3. 验证CDN同源的影响
#

具体实践中,建议另起一个新的同源域名CDN,充分测试影响后,再逐步替换旧的跨域CDN。

量化评估影响时,可以额外统计对开发体验的影响,例如以下方面:

  • 精简了多少行CORS代码、配置;
  • 节省了多少次OPTIONS预检请求;
  • 解决了多少个开发、生产环境的跨域资源报错;

4. 验证压缩算法的影响
#

CDN配置修改前,在本地就可以用Node.js自带的zlib库验证压缩后效果,示例代码:

/* eslint-disable no-console */
const path = require('path');
const zlib = require('zlib');
const { writeFileSync, readFileSync } = require('fs');

const logFileName = 'localLogFile.js';

function getFileContent(filePath = logFileName) {
  let ret = '';
  try {
    ret = readFileSync(path.join(__dirname, filePath), {
      encoding: 'utf 8',
    });
  } catch (err) {
    console.error(err);
  }
  return ret;
}

function writeFile(content, filePath = logFileName) {
  return writeFileSync(path.join(__dirname, filePath), content);
}

function compress(fileName, type = 'gzip') {
  const str = getFileContent(fileName);
  const bufferData = Buffer.from(str, 'utf- 8');
  const inputSize = Buffer.byteLength(str, 'utf 8');
  console.log(`inputSize=${inputSize}`);

  let result = '';

  if (type === 'br') {
    result = zlib.brotliCompressSync(bufferData);
  } else if (type === 'gzip') {
    result = zlib.gzipSync(bufferData);
  } else if (type === 'deflate') {
    result = zlib.deflateSync(bufferData);
  }

  const outputSize = Buffer.byteLength(result, 'utf 8');
  console.log(`${type} outputSize=${outputSize}\n`);

  writeFile(result, fileName.replace('.js', `.${type}.js`));
}

const inputFilePath = './dist/yourJS.min.js';

compress(inputFilePath, 'gzip');
compress(inputFilePath, 'deflate');
compress(inputFilePath, 'br');

这段代码中,我们直接调用Node.js原生的压缩算法API:

  • brotlizlib.brotliCompressSync(bufferData)
  • gzipzlib.gzipSync(bufferData);
  • defaltezlib.deflateSync(bufferData);

对buffer类型的数据进行压缩,并通过Buffer.byteLength(str, 'utf 8');API计算体积,来预测压缩后的体积变化,验证优化效果。

经笔者测试,压缩效果数据如下:

 压缩算法压缩前体积 (单位:byte)压缩后体积 (单位:byte)压缩后体积百分比
gzip示例1: 原体积 100482112482112%
gzip示例2: 原体积 4430935427812%
gzip示例3: 原体积 28955164540023418%
deflate示例1: 原体积 100482112480912%
deflate示例2: 原体积 4430935426612%
deflate示例3: 原体积 28955164540022218%
br示例1: 原体积 1004821904238%
br示例2: 原体积 443093169233%
br示例3: 原体积 28955164334874511% (运行耗时近30s)

2. 量化与评估
#

1. CDN流量开销
#

CDN服务一般基于流量收费,在2023年,下载1GB流量一般收费0.2元左右,上述优化措施中的延长缓存时间,选择最佳压缩算法,都有助于减少流量开销,节省流量花费。

所以我们量化CDN的优化效果时应该优先观察对流量开销的影响。

我们可以利用云服务商提供的后台流量记录或账单,对比优化前后的流量开销,分析优化的效果。

2. CDN资源加载耗时指标
#

为了便于观察CDN优化的效果,我们可以复用在《第3节 光速入门Performance API》中介绍的资源加载耗时指标,来量化CDN域名的连接、加载耗时,从而评估优化效果。

3. 加载资源总体积指标
#

我们还可以进一步利用浏览器 Perfomance API 的transferSize属性建立加载资源总体积指标,上报页面onload事件触发时,加载的所有JS,CSS,图片等资源的总体积。

并通过Grafana将数据可视化,以便于我们在优化前后,对比这一指标的变化,来评估优化的效果。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HTTP Resource Size Metric</title>
    <link
      rel="prefetch"
      href="https://static.zhihu.com/heifetz/6116.216a26f4.7e059bd26c25b9e701c1.css"
    />
  </head>
  <body>
    <h1>2.3.2.3 加载静态资源总体积指标</h1>
    <h2>
      <a href="https://juejin.cn/post/7274889579076108348">
      《1.4秒到0.4秒-2行代码让JS加载耗时减少67%-《现代前端工程体验优化》-第二章-第一节》
      </a>
    </h2>
    <img
      src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5abdb9a058574c2e84c09883ac65541d~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=972&h=550&s=79523&e=png&b=ffffff"
      alt=""
    />

    <script>
      async function reportGauge(name, help, labels, value) {
        await fetch('http://localhost:4001/gauge-metric', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            name,
            help,
            labels,
            value,
          }),
        });
      }

      // 定义资源类型的映射关系
      const resourceTypes = {
        document: 'Document',
        script: 'Script',
        link: 'Link',
        stylesheet: 'Stylesheet',
        img: 'Image',
        media: 'Media',
        font: 'Font',
        xhr: 'XHR',
        fetch: 'Fetch',
        other: 'Other',
      };

      function getResourceSize() {
        // 获取所有资源的性能信息
        const resources = performance.getEntriesByType('resource');

        // 统计各类资源的加载体积
        const resourceSizes = {};

        resources.forEach((resource) => {
          const type =
            resourceTypes[resource.initiatorType] || resourceTypes['other'];

          resourceSizes[type] =
            (resourceSizes[type] || 0) + resource.transferSize;
        });

        // 输出加载体积结果
        console.log('资源加载体积统计:');
        for (const type in resourceSizes) {
          // console.log(`${type}: ${resourceSizes[type]} bytes`);

          reportGauge(
            `ResourceSize`,
            `resource size load of ${type} from front-end project`,
            {
              type,
              id: Date.now(),
            },
            resourceSizes[type],
          );
        }
      }

      setTimeout(() => {
        getResourceSize();
      }, 1000); // 可以按需改成项目完全加载的时间点
    </script>
  </body>
</html>

上述代码逻辑和《CDN域名连接耗时指标》的实现类似,都由调用Performance API、计算数据、上报数据这3步实现:

  1. 调用performance.getEntriesByType('resource') API 获取性能记录数据;
  2. 遍历检查资源加载的性能记录数据,获取其类型(type)、体积(transferSize)等数据;
  3. 最后将数据,通过reportGauge(name, help, labels, value)方法发送HTTP请求到后端数据收集服务;

注意:

对于跨域资源,  需要为对应域名加上 Timing-Allow-Origin: https://your-domain.org 这一专用的HTTP响应头才能正常获取transferSize属性,否则其值将始终为 0。

基于这些数据,我们也可以做出一套可视化图表,量化前端项目的加载资源总体积

image.png
▶点击展开:《加载资源总体积指标》Grafana可视化图表配置源码
    {
      "datasource": {
        "uid": "grafanacloud-prom",
        "type": "prometheus"
      },
      "fieldConfig": {
        "defaults": {
          "custom": {
            "drawStyle": "line",
            "lineInterpolation": "linear",
            "barAlignment":  0, "lineWidth":  1, "fillOpacity":  0, "gradientMode": "none",
            "spanNulls": false,
            "insertNulls": false,
            "showPoints": "auto",
            "pointSize":  5, "stacking": {
              "mode": "none",
              "group": "A"
            },
            "axisPlacement": "auto",
            "axisLabel": "",
            "axisColorMode": "text",
            "axisBorderShow": false,
            "scaleDistribution": {
              "type": "linear"
            },
            "axisCenteredZero": false,
            "hideFrom": {
              "tooltip": false,
              "viz": false,
              "legend": false
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "color": {
            "mode": "palette-classic"
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value":  80   }
            ]
          },
          "unit": "decbytes"
        },
        "overrides": []
      },
      "gridPos": {
        "h":  8,"w":  12,
        "x":  0,"y": 0
      },
      "id":  24,
      "options": {
        "tooltip": {
          "mode": "multi",
          "sort": "none"
        },
        "legend": {
          "showLegend": true,
          "displayMode": "table",
          "placement": "right",
          "calcs": [
            "lastNotNull"
          ]
        }
      },
      "targets": [
        {
          "datasource": {
            "type": "prometheus",
            "uid": "grafanacloud-prom"
          },
          "disableTextWrap": false,
          "editorMode": "builder",
          "expr": "sum(ResourceSize)",
          "fullMetaSearch": false,
          "includeNullMetadata": true,
          "instant": false,
          "legendFormat": "__auto",
          "range": true,
          "refId": "A",
          "useBackend": false,
          "hide": true
        },
        {
          "datasource": {
            "type": "prometheus",
            "uid": "grafanacloud-prom"
          },
          "disableTextWrap": false,
          "editorMode": "code",
          "expr": "avg(ResourceSize)",
          "fullMetaSearch": false,
          "hide": false,
          "includeNullMetadata": true,
          "instant": false,
          "legendFormat": "__auto",
          "range": true,
          "refId": "B",
          "useBackend": false
        },
        {
          "datasource": {
            "type": "prometheus",
            "uid": "grafanacloud-prom"
          },
          "disableTextWrap": false,
          "editorMode": "code",
          "expr": "min(ResourceSize)",
          "fullMetaSearch": false,
          "hide": false,
          "includeNullMetadata": true,
          "instant": false,
          "legendFormat": "__auto",
          "range": true,
          "refId": "C",
          "useBackend": false
        },
        {
          "datasource": {
            "type": "prometheus",
            "uid": "grafanacloud-prom"
          },
          "disableTextWrap": false,
          "editorMode": "code",
          "expr": "max(ResourceSize)",
          "fullMetaSearch": false,
          "hide": false,
          "includeNullMetadata": true,
          "instant": false,
          "legendFormat": "__auto",
          "range": true,
          "refId": "D",
          "useBackend": false
        }
      ],
      "title": "加载资源总体积 ResourceSize",
      "type": "timeseries"
    }

4. 缓存命中率
#

上述CDN优化,对资源的缓存命中率也有显著优化有益,所以继续观测上一节《资源优先级提示优化》中建立的缓存命中率指标,也可以帮我们量化优化效果。

5. TTFB,FCP,LCP,INP等Web Vitals指标
#

上述CDN优化,提高了静态资源的缓存命中率,对页面的加载耗时,加载资源总体积也有显著影响,所以观察优化前后Web Vitals各项指标的变化,也可以衡量我们优化的效果。

例如:

  • 通过使用最新版本的HTTP协议,CDN的连接耗时显著降低,第一字节时间 (Time to First Byte,TTFB)指标就会相应地有所改善。
  • 通过为CDN配置压缩率更高的压缩算法,使得用户首次访问前端页面时,加载的JS,CSS等阻碍渲染的资源体积有所减少,那么预期用户首次内容渲染(FCP)应该也会随之有所优化。

小结
#

在《CDN最佳实践》这2节中,我们介绍了CDN影响用户体验和开发体验的 5大因素:

  1. CDN服务器所在地理位置
  2. CDN缓存配置
  3. CDN域名导致的跨域问题
  4. CDN所使用的压缩算法
  5. CDN 服务器 HTTP 协议版本

及其对应的最佳实践建议:

  1. 选择临近用户的CDN加速区域
  2. 配置最长缓存时间
  3. 让CDN域名符合同源策略
  4. 选择先进的Brotil压缩算法
  5. 使用新版本HTTP协议

最后,针对这些优化方案,验证功能,量化数据和评估效果的细节,提出了各种具体建议。

资源加载优化 - 这篇文章属于一个选集。
§ 3: 本文

相关文章