文章

如何实现全局图片监控

如何实现全局图片监控

为什么要做这个?

  1. 图片过大占用 CDN 资源
  2. 拖慢加载速度,体验不好

要怎么做?

PerformanceObserver 可以获取已缓存图片的 entry 信息,多个相同请求 entry 只会报告一次,能够拿到 decodedBodySize。但在跨域且未使用 Timing-Allow-Origin HTTP 相应标头情况下,这个值为 0 。

主要拿到图片的原始宽高和图片显示的实际宽高,超出一定比例,图片大小超过一定阈值(比如 1M),基本可以判断图片不太符合规范,上报该数据。上报的数据可以是图片的 DOM 路径,用于定位排查。

实现思路

针对不同情况,有不同的监控手段

一些工具函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const getNodeKey = (src: string, path: string[]) => `${src}::${path.join('/')}`;
const getNodeName = (node: Node) => node.nodeName?.toLowerCase() ?? 'unknown';

const isElement = (node: any): node is Element => !!(node.tagName && node.classList);
const isHTMLImageElement = (node: Node): node is HTMLImageElement => getNodeName(node) === 'img';

const getNodePath = (node: Node, path: string[] = []): string[] => {
        if (!isElement(node)) {
            return path;
        }

        const nodeName = getNodeName(node);
        const { id } = node;
        const { className } = node;

        const key = `${nodeName}${id ? `#${id}` : ''}${className ? `.${className}` : ''}`;
        path.push(key);
        return node.parentElement ? getNodePath(node.parentElement, path) : path;
};

利用 PerformanceObserver 获取图片大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const perfWatchSet = new Set(['img', 'css', 'body']);
const perfObserver = new PerformanceObserver(
    list => {
        const entries = list.getEntries();
        for (let index = 0, len = entries.length; index < len; index++) {
           const entry = entries[index] as PerformanceResourceTiming;
        const { initiatorType, encodedBodySize, decodedBodySize, transferSize, name } = entry;
        const src = filterImgSrc(name);
        if (perfWatchSet.has(initiatorType) && src && decodedBodySize > 0) {
        perfEntries.set(src, entry);
        if (transferSize === 0 && encodedBodySize > 0) {
            // 处理逻辑
        }
        },
});

// 浏览器默认是250,不设置大点前面的会被丢弃,监听不到
performance.setResourceTimingBufferSize(2000);
perfObserver.observe({ type: 'resource', buffered: true });

场景一:在 HTML DOM 上的 <img> 标签

分为初始处理和增量处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 工具函数
const getImgSrc = (node: HTMLImageElement) => node.src;
const getBgSrc = (node: Element): string => {
     const { backgroundImage } = window.getComputedStyle(node);
     return ((backgroundImage && regex4BgImage.exec(backgroundImage)) || [])[1] || '';
};

const handleNode = (node: Node) => {
      // 处理 img.src
      handleImageElements(node);
      // 处理backgorundimg style
      handleBgImageElements(node);
};

const visitedNodeSet = new WeakSet<Node>();
const handleNodes = (nodeList: ArrayLike<Node>) => {
    for (let index = 0, len = nodeList.length; index < len; index++) {
        const node = nodeList[index];

        if (visitedNodeSet.has(node)) {
            continue;
        }

        visitedNodeSet.add(node);

        if (isElement(node)) {
            handleNodes(node.children);
            handleNode(node);
        }
    }
}

// 1、初始处理
handleNodes([document.documentElement]);

// 2、增量处理
const observer = new MutationObserver(
(mutations: MutationRecord[])=> {
    for (let index = 0, len = mutations.length; index < len; index++) {
        const mutation = mutations[index];
        handleNodes(mutation.addedNodes);
    }
});

observer.observe(document.documentElement, { attributes: false, childList: true, subtree: true });

还有 img.src 属性变化的情况,也需要用 MutationObserver 监听处理一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 省略处理流程
const imgSrcObserver = new MutationObserver(() => {});

const handleImageElements = (node: Node) => {
        if (isHTMLImageElement(node)) {
            const src = filterImgSrc(getImgSrc(node));
            if (src) {
                imgByImageElement.push({ node, src });
            }
            imgSrcObserver.observe(node, { attributeFilter: ['src'] });
            // 
        }
};

场景二:添加到 HTML DOM 上的有 backgroundImage 的标签

1
2
3
4
5
6
7
8
9
10
11
const handleBgImageElements = (node: Node) => {
      if (isElement(node)) {
         const src = filterImgSrc(getBgSrc(node));
         if (src) {
            imgByBgImageElement.push({ src, node });
         }
         //
  }
};

// 其他代码参考上面场景一

场景三: 使用 API 动态创建

拦截并重写 原生方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const oCreateElement = document.createElement.bind(document);
document.createElement = function (tagName: string, options?: ElementCreationOptions) {
            const newElement = oCreateElement(tagName, options);

            if (isHTMLImageElement(newElement)) {
                handleLoaded(newElement, 'createElement');
            }

            return newElement;
};

const oCreateElementNS = document.createElementNS.bind(document);
document.createElementNS = function (
            namespaceURI: string,
            qualifiedName: string,
            options?: ElementCreationOptions,
) {
            const newElement = oCreateElementNS(namespaceURI, qualifiedName, options);

            if (isHTMLImageElement(newElement)) {
                handleLoaded(newElement, 'createElementNS');
            }

            return newElement;
} as typeof document.createElementNS;

const oImage = window.Image;
window.Image = function (width?: number, height?: number) {
            const newImage: HTMLImageElement = new oImage(width, height);
            handleLoaded(newImage, 'Image');

            return newImage;
} as unknown as typeof window.Image;

// Preserve static properties
Object.assign(window.Image, oImage);

handleLoaded

1
2
3
4
5
6
7
8
9
10
11
12
 function handleLoaded(node: HTMLImageElement, sourceFrom: string) {
      const loadListener = (event: Event) => {
          onLoaded(event, sourceFrom);
      };

      const errorListener = () => {
          //
      };

      node.addEventListener('load', loadListener, { once: true });
      node.addEventListener('error', errorListener, { once: true });
}

以上是基本框架,还有一些上报逻辑,缓存清理逻辑需要补充,完成后便可以得到一个全局的图片监控。

性能

因为全局的监听图片的使用,拿图片的 width、height 还有 backgroundImage 等信息时会强制触发重排重绘,会影响到加载和操作性能,所以不能大批量的全部监控,可以每天挑选部分高性能用户开启,降低影响面。

本文由作者按照 CC BY 4.0 进行授权