如何实现全局图片监控

监控性能优化浏览器

为什么要做这个?

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

要怎么做?

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

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

实现思路

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

一些工具函数

1const getNodeKey = (src: string, path: string[]) => `${src}::${path.join('/')}` 2const getNodeName = (node: Node) => node.nodeName?.toLowerCase() ?? 'unknown' 3 4const isElement = (node: any): node is Element => 5 !!(node.tagName && node.classList) 6const isHTMLImageElement = (node: Node): node is HTMLImageElement => 7 getNodeName(node) === 'img' 8 9const getNodePath = (node: Node, path: string[] = []): string[] => { 10 if (!isElement(node)) { 11 return path 12 } 13 14 const nodeName = getNodeName(node) 15 const { id } = node 16 const { className } = node 17 18 const key = `${nodeName}${id ? `#${id}` : ''}${className ? `.${className}` : ''}` 19 path.push(key) 20 return node.parentElement ? getNodePath(node.parentElement, path) : path 21}

利用 PerformanceObserver 获取图片大小

1const perfWatchSet = new Set(['img', 'css', 'body']); 2const perfObserver = new PerformanceObserver( 3 list => { 4 const entries = list.getEntries(); 5 for (let index = 0, len = entries.length; index < len; index++) { 6 const entry = entries[index] as PerformanceResourceTiming; 7 const { initiatorType, encodedBodySize, decodedBodySize, transferSize, name } = entry; 8 const src = filterImgSrc(name); 9 if (perfWatchSet.has(initiatorType) && src && decodedBodySize > 0) { 10 perfEntries.set(src, entry); 11 if (transferSize === 0 && encodedBodySize > 0) { 12 // 处理逻辑 13 } 14 }, 15}); 16 17// 浏览器默认是250,不设置大点前面的会被丢弃,监听不到 18performance.setResourceTimingBufferSize(2000); 19perfObserver.observe({ type: 'resource', buffered: true });

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

分为初始处理和增量处理

1// 工具函数 2const getImgSrc = (node: HTMLImageElement) => node.src 3const getBgSrc = (node: Element): string => { 4 const { backgroundImage } = window.getComputedStyle(node) 5 return ( 6 ((backgroundImage && regex4BgImage.exec(backgroundImage)) || [])[1] || '' 7 ) 8} 9 10const handleNode = (node: Node) => { 11 // 处理 img.src 12 handleImageElements(node) 13 // 处理backgorundimg style 14 handleBgImageElements(node) 15} 16 17const visitedNodeSet = new WeakSet<Node>() 18const handleNodes = (nodeList: ArrayLike<Node>) => { 19 for (let index = 0, len = nodeList.length; index < len; index++) { 20 const node = nodeList[index] 21 22 if (visitedNodeSet.has(node)) { 23 continue 24 } 25 26 visitedNodeSet.add(node) 27 28 if (isElement(node)) { 29 handleNodes(node.children) 30 handleNode(node) 31 } 32 } 33} 34 35// 1、初始处理 36handleNodes([document.documentElement]) 37 38// 2、增量处理 39const observer = new MutationObserver((mutations: MutationRecord[]) => { 40 for (let index = 0, len = mutations.length; index < len; index++) { 41 const mutation = mutations[index] 42 handleNodes(mutation.addedNodes) 43 } 44}) 45 46observer.observe(document.documentElement, { 47 attributes: false, 48 childList: true, 49 subtree: true, 50})

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

1// 省略处理流程 2const imgSrcObserver = new MutationObserver(() => {}) 3 4const handleImageElements = (node: Node) => { 5 if (isHTMLImageElement(node)) { 6 const src = filterImgSrc(getImgSrc(node)) 7 if (src) { 8 imgByImageElement.push({ node, src }) 9 } 10 imgSrcObserver.observe(node, { attributeFilter: ['src'] }) 11 // 12 } 13}

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

1const handleBgImageElements = (node: Node) => { 2 if (isElement(node)) { 3 const src = filterImgSrc(getBgSrc(node)) 4 if (src) { 5 imgByBgImageElement.push({ src, node }) 6 } 7 // 8 } 9} 10 11// 其他代码参考上面场景一

场景三: 使用 API 动态创建

拦截并重写 原生方法

1const oCreateElement = document.createElement.bind(document) 2document.createElement = function ( 3 tagName: string, 4 options?: ElementCreationOptions, 5) { 6 const newElement = oCreateElement(tagName, options) 7 8 if (isHTMLImageElement(newElement)) { 9 handleLoaded(newElement, 'createElement') 10 } 11 12 return newElement 13} 14 15const oCreateElementNS = document.createElementNS.bind(document) 16document.createElementNS = function ( 17 namespaceURI: string, 18 qualifiedName: string, 19 options?: ElementCreationOptions, 20) { 21 const newElement = oCreateElementNS(namespaceURI, qualifiedName, options) 22 23 if (isHTMLImageElement(newElement)) { 24 handleLoaded(newElement, 'createElementNS') 25 } 26 27 return newElement 28} as typeof document.createElementNS 29 30const oImage = window.Image 31window.Image = function (width?: number, height?: number) { 32 const newImage: HTMLImageElement = new oImage(width, height) 33 handleLoaded(newImage, 'Image') 34 35 return newImage 36} as unknown as typeof window.Image 37 38// Preserve static properties 39Object.assign(window.Image, oImage)

handleLoaded

1function handleLoaded(node: HTMLImageElement, sourceFrom: string) { 2 const loadListener = (event: Event) => { 3 onLoaded(event, sourceFrom) 4 } 5 6 const errorListener = () => { 7 // 8 } 9 10 node.addEventListener('load', loadListener, { once: true }) 11 node.addEventListener('error', errorListener, { once: true }) 12}

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

性能

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