likes
comments
collection
share

还在为表格性能问题发愁?扔掉八股文,结合实际经验谈谈我的看法

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

我正在参加「掘金·启航计划」


某天在群里看到有小伙伴说,页面加载表格需要十几秒,严重影响用户体验,希望大家能集思广益给点建议。关于表格加载卡顿,其实早就是经久不衰的老问题了,大部分前端开发应该都可能会遇到这个问题,于是我也就思考了下相关的解决方法

通用解决手段

表格组件也是组件,一些组件性能优化方法同样可以用在表格组件上,关于组件优化的方法,网上已经有很多了我不想再赘述,这里我说想说一些易于使用且容易被忽略的

避免无效 re-render

表格卡顿绝大部分都是因为 re-render 太多了(特别是初始化的时候),而 re-render 作为一种 DOM 操作是非常消耗性能的,从这方面入手,会起到立竿见影的效果

减少组件级别的 re-render

表格的数据一般都是从接口获取的,如果你在数据返回之前渲染了一遍表格,然后接口返回了又渲染一遍,很显然,前面一次渲染是没必要的,哪怕是渲染一个空的表格,其代价肯定也是大于渲染一个 Loading 组件的

相比于整个表格的loading组件渲染,复杂列组件的 loading会更有显著价值,例如有一列的元素是一个复杂组件,这个组件自身也依赖于异步数据,那么在这个异步数据准备好之前,列组件主内容是没有渲染的必要的,如果表格有100行,这一下子就少re-render100次复杂组件

对于 vue 组件,如果组件是无状态的,那么将组件设置为函数式组件 可以轻易得到性能上的提升;可以根据具体场景尝试 v-memov-once

对于 react 组件,应当避免无意义的 props变更带来的组件re-render,例如 props 不应当是一个对象字面量,在遇到性能瓶颈的前提下,考虑给子组件充当 props的变量使用 useMemouseCallback

减少组件内部引发的 re-render

对于 vue 组件,应当避免把不影响 dom 渲染的变量定义为响应式变量(vue2.x中挂载在 this上的变量,以及 vue3.x中使用 reactiveref 定义的变量),响应式变量的每次变动都会触发响应式的一系列计算,如果这个变量的变动与dom渲染无关,那么这些计算都是没有意义的

对于 react 组件,不影响 dom 渲染的变量不应当使用 useState 定义,考虑使用 useRef代替;如果是与组件实例本身无关的常量或不可变对象,建议将变量定义移动到组件外部

避免无效计算、减少不必要的计算

我曾经写过一个逻辑,基于 react的函数式组件,子组件需要用到父组件内一个formvalues,我直接这么写了

<ChildCom data={form.getFieldsValue()}>

这会导致父组件的每次渲染都会导致一次 form.getFieldsValue()的执行,以及一次 ChildCom 的重渲染,我发现这个问题后,仔细看了下 ChildCom的逻辑,发现只有在少数情况下,才会用到 data,于是我改了下传值方式:

<ChildCom formInstance={form}>

直接把 form 这个引用传进去了,子组件在少部分需要使用 form values的时候,自己调用 props.formInstance.getFieldsValue() 就行了

对于 vue 组件,computed 计算属性代替 method 可以减少响应式数据的不必要计算

对于 react 组件,由于现在我们一般都是使用函数式组件,组件的每次渲染都是把这个组件函数执行了一遍,所以如果组件内存在即时的复杂计算,就会增加性能压力。如果是与状态有关的计算,那么借助 useEffect 仅在依赖变化的时候重新计算;如果计算的结果与状态甚至是组件本身无关,那么建议将变量定义移动到组件外部

// bad
function Com() {
  const data = heavy()
  return <div>{ data.name }<div>
}
// good
// heavy 方法与组件本身状态没有任何关系,那么最好移到组件外面,避免组件重渲染时的无效计算
const data = heavy()
function Com() {
  return <div>{ data.name }<div>
}

虚拟滚动和分段加载、懒加载

我知道,很多人会把虚拟滚动当做是灵丹妙药,在大部分场景下,虚拟滚动确实会取得更好的性能表现,我也建议在遇到性能瓶颈的时候试试这个手段,但没有方法是万能的,虚拟滚动同样会存在它的缺陷

例如,一个表格有 100 行,初始化用时 10s,粗略计算渲染一行的用时是 100ms,再打个折 50ms 好了,如果你使用虚拟滚动,假设设定了每次新加载的行数是 5行,那么即时渲染这 5 行就需要 250ms,在这个时间尺度上,用户实际上已经可以很明显地察觉到了,给用户的体验就是滚动表格滚着滚着忽然卡了一下,然后继续滚,又卡了一下,如果滚的速度快一点,那就是很明显的掉帧体验了

这种体验在我看来,长痛不如短痛,还不如直接一开始卡我十来秒,后续让我顺滑滚动呢,你这一卡一卡的给人感觉是会很难受的,这种情况一般出现在每行的列数比较多,或者列组件比较复杂的时候,每一行的渲染成本本身就比较大,我认为是不适合虚拟滚动渲染的

分段加载,也就是将表格分成几段去加载,比如整个表格共 100行,拿到了100条数据后,先给前 20条数据,等渲染好了,再给前 40条数据,直到最后将 100条谁全部渲染完,这种在我看来缺陷更明显,除非你确认在你的业务场景下,用户会很慢的浏览每一行的数据,有足够的空闲时间让你去分段加载,否则并不建议,因为分段加载会花费更多的总时间和总性能消耗,懒加载也是同样的情况

我不是说虚拟滚动和分段加载、懒加载不行,而是说在使用的时候要先弄清楚它们的适用场景,不要当成灵丹妙药无脑使用

定制化解决

一般情况下,尝试过上述以及其他网上常见的组件性能优化通用解决手段之后,基本上可以缓解大部分卡顿问题,但很明显,既然这个问题经久不衰,肯定是因为还有一些情况不是那么容易解决的,如果真是到了这个地步,那么就只能上定制化解决方案了

尽量复用可复用的组件

表格每一列的组件,一般来说都是同一个组件文件实例化而来的,那么在一些场景下,是没有必要给每一行都实例化一个同样的组件的

比如,表格每行都有一列,这个列的组件内有个按钮,点击了按钮之后,会弹出一个 Modal 组件,如果按照规矩的开发流程,因为每列都会用到这个 ModalModal的渲染内容也是跟这一列的数据相关的,所以每列都会初始化一个 Modal组件等待使用,但实际上全局在同一时刻最多只会有一个 Modal组件被展现出来,那么无论表格有多少行,其实都只需要一个 Modal 就行了

将这个 Modal 在表格外面的组件中初始化一次,当点击列组件的按钮需要弹出 Modal的时候,列组件向外传递一个事件,同时把自己的数据传出去,父组件接收到这个事件后,再根据拿到的数据弹出 Modal并展示这个列组件的数据即可,如果表格有 100 行,相当于是少渲染了 99Modal 实例,还是非常可观的

除了 ModalTooltipNotificationPopoverMessagePopconfirm 等全局同一时刻只会展示一个的组件都可以使用这个方法优化,表格的行数越多、Modal等渲染的内容越复杂,优化表现就越明显

还在为表格性能问题发愁?扔掉八股文,结合实际经验谈谈我的看法

定制专门的表格列子组件

很多表格列使用到的组件可能一开始根本就没考虑过会给表格使用,没考虑过可能会同时被渲染几百个,所以在写组件的时候,根本不会考虑什么性能问题,如果一次只是渲染一个,那么哪怕把性能陷阱全都踩了一遍,可能都不会有什么性能问题,但是当渲染次数变多,量变引发质变的时候,就会立刻发现这个问题了

如果有的组件真的是历史包袱很严重了,不好优化,那么我建议还是重写一遍吧,按照会被渲染几百次的场景进行重写,因为组件真的是写得够烂的话,在足够大的数量级面前,不深入改造是无法取得颠覆式变化的

自定义实现表格

常见的开源组件库 table组件,例如 ant designarco.design,为了满足繁多的使用场景,代码逻辑会比较复杂,你只是想渲染一个简单的表格而已,结果打开DOM检查一看,节点套了一层又一层,甚至table标签都不止一个,除此之外还包含了大量的监听事件,啥都不干光是初始化成本都不小了,还有些逻辑,哪怕你并不需要,if 判断你也避免不了得走一遍,随便动一下就是一大堆的re-render,而这些代价本来是可以不需要付出的

如果经过了上面的一系列尝试后,性能问题依旧存在,那么在你可以确定你的业务场景并不需要开源组件提供的大部分功能,且所用到的功能你完全可以自己实现的前提下,你就可以考虑下自行实现table组件了。哪怕你的技术水平比较挫(也别太离谱奥),比不上那些著名开源组件库的贡献者们,但只要你别自己给自己使绊子,那么我相信,在避免了大量的无效性能付出后,你自己实现的table组件肯定会有更好的性能表现

必须强调的是,只有当你尝试了你能尝试的所有方法都无效后,再考虑这个手段,因为从可维护性上来说,相比于开源组件库,你自己实现的东西更容易劣化

小结

关于组件写得比较差或者缺乏长远规划这件事情,除了个人水平之外,跟业务的迭代速度也有很大的关系,当需求量太大的时候,唯一的目标就是赶在 ddl 之前顺利上线,至于代码是否写得跟 shit 一样,是不会有人关心的,我认为我对于代码质量还是有一定追求的,但在这种氛围下,我也经常写一些我自己都深恶痛绝的代码,对此我还是可以理解的,毕竟技术本就是为业务所服务的,只要能达到业务目标,代码乱一点问题不大

但是如果代码质量已经影响到业务质量了,比如前端卡顿难以操作,后端bug频出,那么就到了需要慢下来整理代码的时候了