如何实现全局图片监控
监控性能优化浏览器
为什么要做这个?
- 图片过大占用 CDN 资源
- 拖慢加载速度,体验不好
要怎么做?
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 等信息时会强制触发重排重绘,会影响到加载和操作性能,所以不能大批量的全部监控,可以每天挑选部分高性能用户开启,降低影响面。