vue复杂列表渲染卡顿优化

问题:对于金融App,表单渲染往往会有较多规则,比如涨了标红、跌了标绿、内容过长字体缩小、缩小到浏览器最小尺寸后缩放、缩放到一定大小后换行等等,包括表头的宽度、高度等都是计算得出。此时会导致一个问题,表单计算的地方太多,当一个列表数据量较大时,首屏渲染就会比较卡顿。

列表组件如下:
在这里插入图片描述

前言

经排查后发现四处可优化地方
✅1.通过refs调用dom并修改样式会导致页面重绘、回流,阻塞页面渲染,应避免
✅2.频繁调用windows全局参数占用资源,应避免
✅3.使用canvas代替临时DOM去计算字体大小
✅4.使用虚拟滚动(vue-virtual-scroller)组件渲染列表


一、避免通过refs调用dom并修改样式

存在通过refs索引修改dom样式的地方,且频繁调用。优化如下:
分析

  • Vue 的 DOM 更新是异步的,可能会批量执行多个 DOM 更新操作,从而避免多次重复渲染。但直接通过 this.$refs 修改 DOM 时,Vue 并不能对这些操作进行批量更新优化,从而增加了渲染开销。而通过refs修改元素样式时,Vue 并不知道这些变化,因此它不能进行优化,可能会触发多次重排和重绘(reflow 和 repaint)。然而金融app的列表里,每个单元格内容的样式都是需要计算的,这种操作会非常频繁,当数据稍微多点问题就更显现了,效率低且导致页面卡顿。

如何优化?

  • 避免通过refs修改元素样式,通过Vue的数据绑定(:class)来动态修改元素的类,Vue 会在数据变化时自动更新 class,并且会触发 Vue 的批量更新机制,会很大程度上提升效率。
  • 通过refs修改元素样式如下
this.$refs[id].style['font-size'] = fontSize + 'px';
  • 通过动态绑定:class修改样式如下
	<div class="default" :class="modFontSize(val)">{{val}}</div>
	/**匹配字体**/
	modFontSize(val) {
	  let fontsize = this.adjustFontSize(val,this.stockW);
	  let fontClass = "";
	  if(fontsize>=14){
	    fontClass = 'm-font-size-14';
	  }else if(fontsize>=12){
	    fontClass = 'm-font-size-12';
	  }else if(fontsize>=10){
	    fontClass = 'm-font-size-10';
	  }else if(fontsize>=8){
	    fontClass = 'm-font-size-8';
	  }else{
	    fontClass = 'm-font-size-two-line';
	  }
	  return fontClass;
	},
	
	adjustFontSize(text, maxWidth) {
	  const canvas = document.createElement('canvas');
	  const ctx = canvas.getContext('2d');
	  let fontSize = 14; // 初始字体大小
	  let fontWidth;
	
	  do {
	    ctx.font = `${fontSize}px Arial`;
	    fontWidth = ctx.measureText(text).width;
	    fontSize--; // 逐渐减小字体大小
	  } while (fontWidth > maxWidth && fontSize > 0);
	
	  return fontSize + 1; // 返回最终的字体大小,这里加1是为了避免字体过小
	},

二、频繁调用window全局参数占用资源

分析

  • window属性在每次访问时,JavaScript 引擎需要先去查找全局对象,然后从全局环境中查找属性,开销相对较大。而this 是指向当前组件实例的对象,通常在方法或计算属性内部进行访问时,查找的范围是有限的,这意味着访问 this 上的属性要比访问 window 的属性更高效。避免频繁调用window属性可减少性能消耗。

如何优化?

  • 经排查发现,在列表的每个单元格里都用到了window.innerWidth值,所以当列表单元格多时就会高频调用。但是window.innerWidth在正常情况下不会改变,无需在每个单元格都调用,在父级组件获取一次,然后通过props传入单元格组件,在单元格组件里用通过prop获取属性值,以此避免频繁调用window属性,提升效率。

三、使用canvas代替零时DOM去计算字体大小

分析

  • 在计算单元格内的字体大小时,通常需要获取到单元格的宽度和单元格内文字的宽度,若内容宽度大于单元格则缩小字体,以此类推。这就说明每个单元格在计算内容字体大小时都需要获取其单元格内文字的宽度,且至少计算一次。此时如果使用临时DOM去计算文字宽度时,性能消耗比使用canvas计算大,此时若计算的地方非常多时性能影响就非常明显了。

如何优化?

  • 通过canvas的getContext方法计算文字宽度,进而计算文字字体和缩放
  • 临时DOM计算如下
 methods: {
  getTextWidth(text) {
    // 创建一个临时的 DOM 元素
    const span = document.createElement('span');
    // 设置字体和大小
    span.style.fontSize = '12px';
    span.style.position = 'absolute'; // 防止影响页面布局
    span.style.visibility = 'hidden'; // 不显示该元素
    span.textContent = text;

    // 将该元素添加到 body 中
    document.body.appendChild(span);

    // 获取文本的宽度
    const width = span.offsetWidth;

    // 销毁临时的 DOM 元素
    document.body.removeChild(span);

    return width;
  }
}
  • 通过canvas计算如下
<template>
  <div>
    <button @click="getWidth">Get Text Width</button>
    <p>Text width: {{ textWidth }} px</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      textWidth: 0
    };
  },
  methods: {
    getTextWidth(text) {
      const canvas = document.createElement('canvas');
      const context = canvas.getContext('2d');
      context.font = '12px Arial';
      const width = context.measureText(text).width;
      return width;
    },
    getWidth() {
      this.textWidth = this.getTextWidth("Hello, Vue!");
    }
  }
};
</script>

四、使用虚拟滚动插件(vue-virtual-scroller)

对应github地址https://github.com/Akryum/vue-virtual-scroller


总结

提示:文章用于记录自己对组件的优化内容,供日后参考,也和大家分享我的解决方法,欢迎指正~

这个问题的场景是:在鸿蒙系统的webview里运行H5的离线包。上述优化后卡顿问题已解决,但是若列表数据过多(如1000条),用户滚动过快时会存在页面短时间空白问题,后续解决了继续更新。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐