性能优化?intersectionObserver 了解一下

2,054 阅读6分钟

缘起

话说某日,项目顺利上线,小仙正嘴里哼着小曲,想趁闲暇时间把手头的插件写完。突然企业微信滴滴响。一看,发现被拉入‘x端内存泄漏,崩溃率上升’群。这还得了,赶紧看看咋回事。

原来,公司某个app(我们活动在该app内打开) top 机型的崩溃率最近几天上升。客户端开发同事一看,崩溃率上升的时间线和我们活动上线的时间线几乎一致!这可不得了,遂马上排查到底是哪个妖魔在捣乱。

寻妖

话说,捉妖先闻气。连接上安卓调试工具,打开我们h5活动页面。刚打开页面内存还算正常,待滚动加载多一点数据后,调试工具的内存曲线图竟然形成爬坡山峰,且毫无下降的趋势,甚至出现app崩溃的情况。仔细一看,主要上升的内存在于Graphic模块。问题原因初步锁定,可能是图片加载过多或过大?遂将活动页面显示列表的头像不予显示,果然内存占用稳定在正常值。这下,妖魔显现,看小仙如何调戏这小妖。

寻法

收妖必用宝,对妖施法,务必做到法到妖收。针对这个问题,小仙根据列表数据加载的情况,首先考虑到可能是头像加载没有使用缩略图资源,导致内存占用上升。但换用缩略图后,数据并没有下降。看来,并不是图片大小的问题,而可能是太多图片资源导致了app内webview的内存上升。当然这个问题可能是webview自身处理图片资源有问题导致本身内存较低的该top 机型内存不足以致崩溃。但是,不管哪边的问题。既然小妖出现,钻了这个漏洞,那咱们就想办法先捉住这只小妖好好玩玩。既然图片过多,那咱们就只让用户看得到的图片才渲染,其他的一律不渲染。

法器介绍

小仙本次使用的法器为,intersectionObserver API,待我介绍一下这门法器

intersectionObserver

根据MDN的说法,intersectionObserver接口提供了一种异步观察目标元素与其祖先元素或顶级文档视窗之间交叉状态的方法,祖先元素与视窗被称为根元素(intersectionObserver)。换成人话就是,我们可以用这个接口来判断两个元素是否相交,且根元素必须是目标元素的祖先(即兄弟之间是不能互相监督的,只有爸爸以上才能看管自己的孩子,还不能父子颠倒,本末倒置)。

使用

  1. 首先,我们使用IntersectionObserver构造函数实例化一个观察者,用来观察两个元素是否相交。构造函数接收两个参数,一个是可变性变化时触发的回调函数callback,可以在这个函数中做一些你想要的操作。一个是可选的配置对象options
const observer = new IntersectionObserver((entries, options) => {
    console.log(entries);
}, {
    rootMargin: '200px 0px 60px 0px', // 目标元素距离根元素的外边框多远就触发相交, 本例中,目标元素在小于根元素上边框200px(从上到下时)或距离下边框小于等于60px(从下到上)时触发相交
    threshold: 0.1 // 当目标元素进入根元素区域达到多少比例时触发相交事件,可以是number也可以是number数组,如[0.1, 0.4, 0.75, 1]表示目标元素进入根元素分别达到10%,40%,75%,100%时都会触发一次回调函数
    root: document.getElementById('#parent') // 根元素(默认为顶级视窗元素)
});

callback函数接收IntersectionObserverEntry对象数组,其主要属性如下:

boundingClientRect:目标元素矩形区域的信息
intersectionRect: 目标元素与根元素相交的矩形区域信息
rootBounds: 根元素的矩形区域信息
intersectionRatio: 目标元素与根元素相交比例,即intersectionRect 占 rootBounds的比例,此值在0-1之间,未相交时为0,完全相交时为1
isIntersecting: 目标元素与根元素是否相交
time: 可见性发生变化时的时间戳,单位毫秒,chrome上精确到小数点后10位
target: 目标元素的dom对象

  1. 观察目标元素:
const target = document.getElementById('target');
observer.observe(target);

至此,我们的observer已经开始监测目标元素,当目标元素与根元素相交时,便会触发我们上面提到的callback。需要注意的是,callback是一个异步函数,其底层使用了原理类似于requestIdlecallback的方式执行这个回调,任务优先级比较低,所以这个函数会有延迟。在懒加载图片过程中如果滑动的快的话,可能会比较慢出现图片,对于这种情况,我们可以稍微增大实例化时传入options中的rootMargin的边距,以提前一点加载相应数据。具体增大哪个值,需要看具体需求。

  1. 停止观察元素
observer.unobserve(target)

一般在页面卸载之前需要unobserve(比如vue的beforeDestroy钩子函数中进行unobserve),以避免导致内存泄漏。

  1. 关闭观察器,
observer.disconnect();

施法

为简单演示,我们部分代码省略 container.vue

<div class="container">
    <row v-for="item in itemList" :thump-url="item.src"></row>
</div>
export default {
    data() {
        return {
            itemList: [{url: 'xxx'}, ...],
            observer: null
        }
    },
    mounted() {
        this.initObserver();
    },
    beforeDestroy(){
      this.observer.disconnect();  
    },
    methods: {
        initObserver() {
            this.observer = new IntersectionObserver((entries, options) => {
                entries.forEach(this.isIntersectHandler);
            }, {
                rootMargin: '200px 0px 60px 0px',
                threshold: 0.1
            });
        },
        isIntersectHandler(entry){
            let target = entry.target;
            const isIntersecting = entry.isIntersecting;
            let thumpTarget = target.getElementsByClassName('thump')[0];
            if (isIntersecting) {
                const imgUrl = thumpTarget.dataset['src'];
                imgLoadHandler(thumpTarget, imgUrl); // imgLoadHandler内部使用了new Image,事先下载url的资源,当加载完成后就将url赋值到thumpTarget的src上,发生错误或还未加载完则使用默认的头像
            } else {
                thumpTarget.src = ''; // 把目标src置空,是为了在非可视区域不渲染这个图层
            }
        }
    }
}

row.vue(简略代码)

<div class="row" ref="row">
    <img class="thump" data-src="thumpUrl">
</div>
export default {
    props: {
        thumpUrl: {
            default: '',
            type: String
        }
    },
    mounted(){
        this.$parent.observer.observer(this.$refs.row);
    },
    beforeDescroy(){
        this.$parent.observer.unobserver(this.$refs.row);
    }
}

法术效果

经过小仙略施小法,在android studio 调试中看到,我们的内存不再持续上升,且加载完所有数据后,整个app占用内存也从之前的平均280M下降到了平均220M。此次施法可以说成效初显。

兼容性

目前较新版的浏览器基本支持,但是一些旧版本浏览器则不支持,可以使用intersection-observer插件进行polyfill。

后记

本次上线后,相关app的崩溃率其实并没有下降。但联系到我们的活动其实已经下线了,所以可以确定问题不在这次h5页面上,而是在客户端相关的优化没有做好。虽然这样,我们前端依然进行优化并记录,以提升整个团队的能力。 小仙第一篇文章,还请大神们多多指教。

参考文献

  1. 阮一峰-IntersectionObserver API 使用教程
  2. MDN-Intersection Observer