likes
comments
collection
share

关于ResizeObserver,你还可以这样做性能优化...

作者站长头像
站长
· 阅读数 10

先说说ResizeObserver

ResizeObserver的作用主要是用来监听DOM元素的尺寸变化,在这个API还没有诞生时,通常需要通过CSS媒体查询,或者通过window上的resize事件间接监听整个视窗的变化。

但在有些情况下,视窗尺寸的改变不一定导致监测元素尺寸的变化,或者,监测元素尺寸的变化并不由视窗尺寸元素的变化导致。例如,自身支持resize的元素在响应式布局中影响其他元素,支持动态增删改的列表也会导致父元素的尺寸变化。这种时候,监听视窗的尺寸变化,不仅浪费性能,还有可能起不到想要的监听效果。

ResizeObserver可以做到,不管什么层级的DOM元素,调用它的observer方法,即可精准监听尺寸变化。不用再去憨憨地监听整个视窗的变化了。

同时,这也是一种无侵入的观察方式,可以在完全不影响目标元素业务逻辑的同时,嵌入需要响应变化的功能逻辑,例如发送埋点数据。减轻了开发者的心智负担和维护成本。

目前基本是现代浏览器全线支持:

关于ResizeObserver,你还可以这样做性能优化...

ResizeObserver的基本使用

ResizeObserver本身是一个构造函数,通过调用它创建一个实例。

同时需要传给它一个callback,在callback中写好当目标元素尺寸发生变化时,需要相响应的逻辑。

callback本身接收一个变化对象数组参数,每个对象中包含了尺寸变化的相关信息。

const observer = new ResizeObserver(callback);

function callback(entries){
    for(const entry of entries){
        const rect = entry.contentRect;
        // ...
    }
}

其中,entry.contentRect返回的是一个DOMRect对象,其中包含了目标元素的x, y, width, height, top, bottom, left, right等属性。

ResizeObserver有三个方法,observe(),unobserve(),disconnect(),分别对应了开始监听,结束监听,以及取消所有对目标元素的的监听。

// 获取目标元素
const ele = document.getElementsByClassName('list-item');
// 开始监听
observer.observe(ele);
// 结束监听
observer.unobserve(ele);
// 取消监听所有目标元素
observer.disconnect();

通常,在完成监听功能后,我们需要及时调用unobserve(),disconnect()方法,防止产生内存泄漏问题。

全局唯一实例的性能优化

其实,ResizeObserver的诞生,相较于MutationObserver以及传统的resize事件,本身就是高效的尺寸变化监测。它的精准性避免了很多不必要的性能开销。

但在最近的一个项目中,由于业务需求,需要通过ResizeObserver监听动态列表项高度,将它加在了一个长列表的每一项。由于每一项都是一个单独的组件,每个组件都拥有自己的 ResizeObserver 实例,这意味着浏览器会为每个实例分配内存和处理事件。当页面上有大量组件时,会对性能产生相当程度的影响。

细心的读者应该发现了,每个ResizeObserver实例是可以同时监听多个目标元素的,那么借鉴单例模式的思想,尝试封装一个全局的ResizeObserver,限制整个项目只有一个实例。在需要调用的地方直接GlobalResizeObserver.observe()!话不多说,上代码:

export const GlobalResizeObserver = (function() {  
    const ATTR_NAME = 'global-resizeobserver-key';  

    const attrValueToCallback = {};  

    const o = new ResizeObserver((entries) => {  
        for (const entry of entries) {  
            const resizedElement = entry.target;  
            const attrValue = resizedElement.getAttribute(ATTR_NAME);  
            if (attrValue) {  
                const callback = attrValueToCallback[attrValue];  
                if (typeof callback === 'function') {  
                    callback(entry);  
                }  
            }  
        }  
    });  
  
    return Object.freeze({  
        /**  
        * @param { Element } element  
        * @param { (ResizeObserverEntry) => {} } callback  
        */  
        observe(element, callback) {  
            if (!(element instanceof Element)) {  
                console.error('GlobalResizeObserver, cannot observe non-Element.');  
                return;  
            }  

            let attrValue = element.getAttribute(ATTR_NAME);  
            if (!attrValue) {  
                attrValue = String(Math.random());  
                element.setAttribute(ATTR_NAME, attrValue);  
            }  

            attrValueToCallback[attrValue] = callback;  
            o.observe(element);  
        },  

        /**  
        * @param { Element } element  
        */  
        unobserve(element) {  
            if (!(element instanceof Element)) {  
                console.error('GlobalResizeObserver cannot unobserve non-Element.');  
                return;  
            }  
            const attrValue = element.getAttribute(ATTR_NAME);  
            if (!attrValue) {  
                console.error('GlobalResizeObserver cannot unobserve element w/o ATTR_NAME.'); 
                return;  
            }  
            delete attrValueToCallback[attrValue];  
            o.unobserve(element);  
        }
    });  
})();

主要思路是通过JS的闭包特性,构建对象保存每个被监听元素对应的callback。

通过构建全局唯一的ResizeObserver实例,页面的fps, Response Time, CPU Usage, Memory Consumption, Event Handling等性能指标都有了提升!

当然,如果监听的目标元素足够多,相应的给callback函数加上防抖节流也是很有必要的,毕竟前端页面性能直接关乎用户体验的好坏。

在React中的使用

不管是原生的生成实例用法,还是封装全局唯一实例,在React中的使用应该不存在什么最佳实践,仁者见仁,智者见智。这里仅介绍一种回调ref的做法,供大家参考。

import React, { useCallback } from 'react';
import { GlobalResizeObserver } from 'global-resize-observer';

export const component = () => {
    const callbackRef = useCallback((node) => {  
        if (!node) {  
            GlobalResizeObserver.unobserve(node);  
            return;  
        }          
        GlobalResizeObserver.observe(node, (entry) => {  
            // 响应逻辑...  
        });  
    }, []);
    
    return <div ref={callbackRef}></div>
}

callbackRef会将当前ref的值作为函数入参传入,并通过useCallback封装,并且依赖数组为[],所以callbackRef会在组件初始化和卸载阶段被调用,执行绑定和解绑逻辑,不失为一种优雅调用方法~

有类似场景的同学不妨给自己的项目封装一个试试吧!