为什么要做这个?
- 图片过大占用 CDN 资源
- 拖慢加载速度,体验不好
要怎么做?
PerformanceObserver 可以获取已缓存图片的 entry 信息,多个相同请求 entry 只会报告一次,能够拿到 decodedBodySize。但在跨域且未使用 Timing-Allow-Origin
HTTP 相应标头情况下,这个值为 0 。
主要拿到图片的原始宽高和图片显示的实际宽高,超出一定比例,图片大小超过一定阈值(比如 1M),基本可以判断图片不太符合规范,上报该数据。上报的数据可以是图片的 DOM 路径,用于定位排查。
实现思路
针对不同情况,有不同的监控手段
一些工具函数
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 获取图片大小
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) { } }, });
performance.setResourceTimingBufferSize(2000); perfObserver.observe({ type: 'resource', buffered: true });
|
场景一:在 HTML DOM 上的 <img>
标签
分为初始处理和增量处理
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) => { handleImageElements(node); 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); } } }
handleNodes([document.documentElement]);
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 监听处理一下
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 的标签
const handleBgImageElements = (node: Node) => { if (isElement(node)) { const src = filterImgSrc(getBgSrc(node)); if (src) { imgByBgImageElement.push({ src, node }); } } };
|
场景三: 使用 API 动态创建
拦截并重写 原生方法
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;
Object.assign(window.Image, oImage);
|
handleLoaded
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 等信息时会强制触发重排重绘,会影响到加载和操作性能,所以不能大批量的全部监控,可以每天挑选部分高性能用户开启,降低影响面。