• home > webfront > SGML > html5 >

    图表列表性能优化:可视化区域内最小资源消耗

    Author:zhoulujun Date:

    本来想标题党:一个组件的自我修养之路。其实就是通过 (Intersection Resize)Observer+getBoundingClientReact去优化图表列表性能,给新手指出其中的注意事项。

    之前写过《懒加载优化:JavaScript IntersectionObserver API监听元素是否可见》,基于上一篇文章,做个滚动懒加载完全不是问题。

    但是,如果页面定时自动刷新,不可见区域内的刷新完全是浪费前后端的资源。

    本篇在上篇的基础,通过自己的一个改版案例,来看IntersectionObserver+ResizeObserver+getBoundingClientReact+Object.freeze是如何提升项目的整体性能与用户体验的

    案例如下:

    WeChatWorkScreenshot_e685b007-6459-4c46-974a-b5c114cdfba4.png

    这个页面,不只是简单的滚动加载那么加载。图表也比较复杂

    • 刷新页面操作:切换右侧目录列表、搜索确定、查询搜索、面板手动刷新、面板设置定时自动刷新

    • 刷新图表事项:父子图、关联图、组合图(图表套图表)

    • 尺寸调整事项:浏览器页面尺寸调整、侧边栏收起、侧边栏尺寸拖曳调整,编辑模式下:分组尺寸调整、图表尺寸调整

    这个页面之前的实现的挺复杂,而且时不时的报bugger(代码复杂了,出问题的概率肯定会加大)。

    来看看你的项目存是否也可能存在以下几个致命问题:

    1. 多图表的列表,多用户设置定时自动刷新,服务器请求特别多,资源消耗严重(如果限制视窗内刷新,十屏滚动,资源就是减少90%)

    2. 图表列表数据过大时,页面卡死,甚至崩溃(

      1. BUS、echarts事件组件注销时没有解绑——函数多次重复执行

      2. 图表数据Vue 深度watch——大数据图表,CPU、内存爆棚,页面直接崩溃

    3. 页面整体事件响应慢——父容器不断遍历通知子组件,性能消耗。

    4. echarts图表刷新慢——很多时候echarts实例重建,而不是调用原来的实例 setOption 

    5. 定时刷新时间不精准,内存泄露——setInterval直接设置定时刷新

    6. windows全局手动管理echarts实例,项目内存占用巨大,甚至内存泄露,页面崩溃

    直接开干版

    容器滚动,通知容器内组件,需要重新渲染;组内再调用组件内刷新。

    同理,当父容器尺寸变化时;或者编辑列表,尺寸调整时;做同样的操作。

    这个就是原来的实现方式

    面板页面组件:贴出来肯定是简化版的,实际业务复杂得多得多……

    <template>
      <div class="list-box">
        <Group
          v-for="group in list"
          :key="group.id"
          ref="group"
        />
      </div>
    </template>
    <script>
    export default {
      watch: {
        isSidebarOpen() {
          this.handleRenderDebounce();
        },
      },
      mounted() {
        // debounce 优化性能后,还是会冗余渲染
        this.handleRenderDebounce = tools.debounce(this.handleRender, 200);
        // 父容器监听 滚动事件,触发渲染函数
        this.$refs.listBox.addEventListener('scroll', this.handleRenderDebounce);
        // 页面尺寸调整时,触发滚动函数
        window.onresize = this.handleRenderDebounce;
        
        // 重置条件/搜索
        Bus.$on('reloadGroupChart', () => {
          if (this.$refs.group) {
            this.$refs.group.forEach((group) => {
              if (group.resetLoadMap) {
                group.resetLoadMap(true);
              }
            });
            this.$nextTick(() => {
              this.handleRender();
            });
          }
        });
        // 新建分组,滚动到新分组位置
        Bus.$on('eventScrollToNewGroup', () => {
          if (this.$refs.group && this.$refs.group.length > 0) {
            this.$refs.group[this.$refs.group.length - 1].$el.scrollIntoView();
          }
        });
      },
      methods: {
        handleRender() {
          // 每个分组遍历 调用其 渲染函数
          this.$refs.group.forEach((el) => {
            el.handleRender();
          });
        },
      },
    };
    </script>
    
    <style lang="scss">
    
    </style>

    然后再每个图表组件,再次感觉被轮奸一遍

    <template>
      <div class="list-box">
        <chart-item
          v-for="chart of group.charts"
          ref="chart"
          :key="chart.id"
        />
      </div>
    </template>
    
    <script>
    export default {
      mounted() {
        Bus.$on('eventRefreshCharts', () => {
          for (const chart of this.$refs.chart || []) {
            chart.initChartItem();
          }
        });
        Bus.$on('eventRefreshTargetChart', (chartId) => {
          this.$forceUpdate();
          this.$nextTick(() => {
            for (const chart of this.$refs.chart || []) {
              if (chart.chart.id === chartId) {
                chart.clickRefreshChart();
              }
            }
          });
        });
        Bus.$on('eventRefreshCharts', (_) => {
          for (const chart of this.$refs.chart || []) {
            chart.initChartItem()
          }
        })
        if (this.group.charts) {
          this.group.charts.forEach((chart) => {
            this.$set(this.reloadMap, chart.id, false);
          });
        }
        this.$nextTick(() => {
          // 规避 - 初始化时chart大小位置计算不准确的问题
          setTimeout(() => {
            this.handleRender();
          }, 300);
        });
      },
      methods: {
        // 触发chart加载或更新
        handleRender() {
          if (this.$refs.chart) {
            this.$refs.chart.forEach((chart) => {
              if (chart.handleRepeater) {
                chart.handleRepeater();
              }
            });
          }
        },
        // 重置加载状态
        resetLoadMap(isNeed, chartId) {
          if (chartId) {
            this.reloadMap[chartId] = isNeed;
          } else {
            for (const key in this.reloadMap) {
              this.reloadMap[key] = isNeed;
            }
          }
        },
      },
      // 图表展开,重新触发加载
      toggleGroup() {
        this.group.isExpand = !this.group.isExpand;
        this.$nextTick(() => {
          this.$emit('handleRender');
        });
      },
      // 分组尺寸调整时,……
    };
    </script>

    然后又在每个图表组件里面,去重新渲染子组件。

    这个代码就不贴了……

    上面代码基本实现了上述的功能,但肯定不符合 高内聚低耦合 的,都俄罗斯套娃了。


    自我管理版

    先概括地说一下优化思路:

    1. 对于滚动加载,有IntersectionObserver API,滚动时,组件自己判断是否可见,去加载。但是,这里面还要注意下条件

      1. 未初始化时,滚动时候,直接加载就是。并存储当前加载的请求参数,以后后面加载时核验

      2. 已经加载中(组件loading时),无需再加载)

      3. 已经初始化了,需要判断查询条件是否改变,如果改变了,需要再次加载——如查询参数、定时刷新时间

    2. 对于尺寸变化,有ResizeObserver,无论是页面尺寸变化、还是其父组件、爷爷组件尺寸变化,都会反馈到之间本身的尺寸变化,直接监听组件本身就好。

    3. 对于刷新事件,组件自己储备上次加载的参数,接手刷新事件后,自己觉得干啥。

    4. 对于内存CPU+内存爆炸,杜绝图表配置项(option参数)在vue上绑定与监听,可以数据采样;echarts实例、各类绑定事件,及时销毁。

    在vue实现上,可以是个公用的基础类,其他图表组件去继承这个类。也可以是一个抽象组件。

    下面是算是伪代码级别吧

    <template>
      <div v-bkloading="{isLoading:loading&&uninitialized}" class="chart-box" ref="chart">
        <!--图标标题,首次加载,整个图表loading,再次加载,只有标题展示loading Gif图片-->
        <chart-title
          :loading="loading"
          @refreshChart="clickRefreshChart"
        />
        <!--如果echarts图表封装成组件,不建议通过prop传递option参数(要做也先数据冻结-Object.freeze(option))
        千万不要deep watch option,大数据直接奔溃。-->
        <e-chart ref="eChartRef" />
        <!--图表为渲染前,默认占位图-->
        <img v-else>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          // 组件未初始化(已经初始化,再次请求,echarts setOption即可
          uninitialized: true,
          // 数据加载中
          loading: true,
          // 可给定echarts 图表模式示范数据,先展示,等数据请求完毕在展示
          option: null,
          clickRefreshChart: null,
          intersectionObserver: null,
          resizeObserver: null,
        };
      },
      beforeDestroy() {
        // 注意内存泄露问题
        // this.intersectionObserver.unobserve(this.$el)
        // 建议直接使用 disconnect
        this.intersectionObserver.disconnect();
        this.intersectionObserver = null;
        this.resizeObserver.disconnect();
        this.resizeObserver = null;
        // echart 手动释放资源
        if (this.eChart) {
          this.eChart.clear();
          this.eChart.dispose();
          this.eChart = null;
        }
      },
      mounted() {
        this.intersectionObserver = new IntersectionObserver((entries) => {
          if (entries[0].intersectionRatio > 0) {
            this.initChartItem();
          }
        });
        this.observeIo.observe(this.$el);
        this.resizeObserver = new ResizeObserver((entries) => {
            // 渲染图表,根据产品情况,考虑是否防抖、节流
        });
        this.resizeObserver.observe(this.$el);
        /* const isElementNotInViewport = function (el) {
          const rect = el.getBoundingClientRect();
          return (
            rect.top >= (window.innerHeight || document.documentElement.clientHeight)
            || rect.bottom <= 0
          );
        };*/
        /**
         * 刷新图表,值刷新可视化区域内的图标
         */
        Bus.$on('clickRefreshChart', (data) => {
          if (!isElementNotInViewport(this.$el)) {
            this.clickRefreshChart(data);
          }
        });
        this.clickRefreshChart = debounce((data) => {
          // TODO 不同的刷新操作,逻辑
          this.initChartItem(data);
        }, 700);
      },
      methods: {
        // 触发chart加载,更新图标
        async initChartItem() {
          // 在loading中,不重新加载
          if (this.loading) {
            return;
          }
          // 以及初始化,但是查询条件没有刷新,不重新加载
          if (!this.uninitialized) {
            if (this.context.start_time === this.contextBak.start_time
              && this.context.end_time === this.contextBak.end_time) {
              return false;
            }
            // TODO 其他条件等等
          }
          // TODO 加载数据,渲染图表
        },
        // 渲染echarts图表
        renderEchart(){}
      },
    };
    </script>

    上面比较刷新条件,最简洁的方式就是,触发刷新面板条件,存储一个时间戳。当滚动加载的时候,比较时间戳,觉得是否再次加载。


    优化,x效果俺是比较满意。

    感觉文章写的不是很清楚,但是项目代码是不能直接露的,先这样的吧,后面再补充

    欢迎道友们共同探讨,贫道有礼了……





    转载本站文章《图表列表性能优化:可视化区域内最小资源消耗》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/SGML/html5/2021_0619_8640.html