antd 表单性能的改进实践

2,499 阅读8分钟
原文链接: zhuanlan.zhihu.com

摘要

随着es2015、babel等技术在前端迅速普及,前端开发效率大幅提升。vue、react之类的框架也被广泛采用。在使用基于react的antd构建后台系统的过程中遇到了明显的性能问题。下面针对遇到的性能问题进行研究并给出解决方案。

antd简介

antd是由阿里旗下的 蚂蚁金服体验技术部 开发的一套UI设计语言,包括了设计样式、规范、组件库等内容。antd基于React开发,并且是起步较早的框架,因此功能上比较完善,使用者较多,社区资源也多。因此在去年技术选型中选中了antd作为DA后台系统前端的主要框架之一。

性能问题

所有的框架在降低开发成本的同时必然会牺牲一部分的性能,antd 也不例外。虽然 antd 基于的React生态一直在积极的优化性能,然而在极端情况下依然会出现性能问题。

具体问题分析

antd 的性能问题大致可以归为几类,

  1. 带有复杂动画的组件大量出现在同一页面
  2. 单个页面里同时展示多个循环列表
  3. 循环列表中将ReactElement传入另一个React组件中
  4. 每次触发更新都引起了整个页面的更新
  5. 用户使用了省电模式,或者cpu资源比较紧张

一般满足上面两条以上页面就会有可观的性能瓶颈。 在测试页面性能前,应该把电脑调节为省电模式,这样就能充分暴露页面性能是否有瓶颈。

测试环境

  1. 型号: Thinkpad x260 笔记本
  2. CPU: intel(R) Core(TM) i5-6200U CPU @ 2.3 GHz 2.4 GHz
  3. 内存: 8 GB
  4. Chrome 版本: 61.0.3135.5 (正式版本)dev (64 位)

模块版本

// package.json

{
  ...
  "dependencies": {
    "antd": "^2.11.2",
    "babel-polyfill": "^6.23.0",
    "camel-case": "^3.0.0",
    "classnames": "^2.2.5",
    "es6-promise": "^4.1.1",
    "history": "^4.6.3",
    "immutable": "^3.8.1",
    "isomorphic-fetch": "^2.2.1",
    "matchmedia-polyfill": "^0.3.0",
    "moment": "^2.18.1",
    "prop-types": "^15.5.10",
    "qs": "^6.5.0",
    "react": "^15.6.1",
    "react-dom": "^15.6.1",
    "react-redux": "^4.4.8",
    "react-router": "^4.1.1",
    "react-router-dom": "^4.1.1",
    "react-router-redux": "^5.0.0-alpha.6",
    "react-trigger-change": "^1.0.2",
    "redux": "^3.7.1",
    "redux-thunk": "^2.2.0",
    "request": "^2.81.0",
    "request-promise-native": "^1.0.4",
    "should-update": "^2.0.0",
    "urijs": "^1.18.10",
    "uuid": "^3.1.0"
  },
  ...
}

准备工作

  1. Demo 地址:github.com/assweeecan/… 。运行后打开http://localhost:8330/ 。
  2. Demo 在线演示:assweeecan.github.io/antd_perfor…
  3. 安装新版 Chrome
  4. 学会使用 Performance (老版本里是 Timeline )

<img src="https://pic3.zhimg.com/v2-6c933a29ca72a879b1fd52417313a56e_b.png" data-rawwidth="748" data-rawheight="613" class="origin_image zh-lightbox-thumb" width="748" data-original="https://pic3.zhimg.com/v2-6c933a29ca72a879b1fd52417313a56e_r.png">

注:图例中黄色的 Scripting 指的是 js 运行时间;紫色的 Rendering 指的是计算页面元素之间的关系的时间,即渲染时间;绿色的 Painting 是绘制时间,即图像显示出来的时间。 在测试中,Scripting 时间太长通常是 React 的 diff 过程耗时较多,Rendering 时间太长则是 Dom 操作太多。 开启 performance 面板会消耗大量资源 具体使用方法自行搜索
  1. 测试方法是:
1. 电脑设置成省电模式,为了让耗时更加明显
2. 打开测试页面
3. 打开控制台 -> Performance
4. 关闭 Screenshots 自动截屏选项,因为截屏会消耗大量资源
5. 点击“开始录制”
6. 点击页面中的"开始测试",按钮文字变为“测试中”
7. 等待按钮变回“开始测试”,点击控制台中的“停止录制”
8. 查看性能分析图

Checkbox 选择框

选择框是一种非常常用的表单组件,antd 也提供了包装过的选择框。

然而将大量选择框放在同一页面时就会产生明显的延迟感 通常我们会像 antd 的例子一样使用 checkbox ,但是同一页面超过 500 个 checkbox 就会发现响应便慢。

示例页面:assweeecan.github.io/antd_perfor…

性能测试图:

<img src="https://pic3.zhimg.com/v2-be920f92130ed9835fc6c2e6f614be9e_b.png" data-rawwidth="755" data-rawheight="936" class="origin_image zh-lightbox-thumb" width="755" data-original="https://pic3.zhimg.com/v2-be920f92130ed9835fc6c2e6f614be9e_r.png">

可以看到 Rendering 使用了 2808ms,瓶颈在 Dom 操作上。

而大部分时间都在播放动画,说明这很有可能是动画导致的卡顿。仔细观察,发现 checkbox 在选中和取消的时候是有动画的,查看 css 代码,果然使用了一个比较复杂的 css 动画。那么继续测试,修改 css 样式去掉它的动画。

示例页面:assweeecan.github.io/antd_perfor…

性能测试图:

<img src="https://pic3.zhimg.com/v2-060c28878f03ddd8a80d444f545566a6_b.png" data-rawwidth="750" data-rawheight="957" class="origin_image zh-lightbox-thumb" width="750" data-original="https://pic3.zhimg.com/v2-060c28878f03ddd8a80d444f545566a6_r.png">

对比发现,Rendering 减少为 1445ms ,实际体验一下勉强可以接受。而动画效果并不是特别显眼,关闭动画并不会引起用户的注意。

因此,在页面需要展示大量 checkbox 的时候,可以关闭 checkbox 的动画,这样可以提升用户体验。

表单中的复杂内容

在 Form 表单中不一定都是输入组件,也会包含展示组件。但是由于 Form 更新时会触发所有子组件的更新,所以表单里包含了一个很大的组件,例如 Tabs 数组,就会造成性能瓶颈。

示例页面:assweeecan.github.io/antd_perfor…

性能测试图:

<img src="https://pic4.zhimg.com/v2-079cfea150db307e4e96a46999b0388f_b.png" data-rawwidth="745" data-rawheight="942" class="origin_image zh-lightbox-thumb" width="745" data-original="https://pic4.zhimg.com/v2-079cfea150db307e4e96a46999b0388f_r.png">

在使用 Form.create 的时候会给组件包裹一层 Form 组件。使用 this.props.form.getFieldDecorator 注册的组件会在 Form 的 state 中保存相应的数据,包括 value 、校验状态、错误信息。当表单的组件触发 onChange 时,Form 的 state 就会发生改变,触发界面更新的同时也会触发子组件的更新。如果组件包含大量的循环节点,本身渲染就很慢,就会影响表单的性能。而 Input 对性能是最敏感的,所以会出现输入卡顿的现象。

控制台打印的是整个 Form 组件 componentWillUpdate 和 componentDidUpdate 的时间差,可以看到图中 scripting 花费了 1378ms, 打印出来的更新时间差也达到了 200ms以上,这足以造成输入卡顿。而组件更新时间小于 16ms 时用户输入才不会有卡顿感。你也可以自己试试看卡顿到了什么程度。

将 tag 拆分成一个组件,并设置 shouldComponentUpdate。这样每次输入时就不会触发 tag 的更新。

示例页面:assweeecan.github.io/antd_perfor…

性能测试图:

<img src="https://pic3.zhimg.com/v2-c05d723851246875f21d1f18b04cef16_b.png" data-rawwidth="736" data-rawheight="906" class="origin_image zh-lightbox-thumb" width="736" data-original="https://pic3.zhimg.com/v2-c05d723851246875f21d1f18b04cef16_r.png">

打印时间差已经缩短到 16ms 以内,用户不会再感受到卡顿。因此,在表单内容比较多的时候必须注意这些数量多但是又不经常更新的内容,将其拆分成子组件是一个好办法。

表单中的复杂的域组件

复杂的表单域组件也会影响性能。上面说了表单中的组件过于复杂的组件是会影响性能的。例如 antd 的选择框,在传入选项的时候使用了 Option 组件传入选项参数。由于 ReactElement 会在每次 Render 的时候生成新的对象,因此组件就无法直接判断传入参数数组是否有变化,也就是每次都会重新生成索引、生成界面。如果表单里有 Input 组件,用户在输入时就会觉的卡顿。

示例页面:assweeecan.github.io/antd_perfor…

性能测试图:

<img src="https://pic2.zhimg.com/v2-01faca9d58cd3cdf7ecf11971b4f2bfd_b.png" data-rawwidth="748" data-rawheight="963" class="origin_image zh-lightbox-thumb" width="748" data-original="https://pic2.zhimg.com/v2-01faca9d58cd3cdf7ecf11971b4f2bfd_r.png">

Select 就是一个比较复杂的表单组件,我们可以看到,在 Input 中输入文本时 js 处理花费了大量时间。解决这个问题的方法就是寻找一个高性能的 Select 组件替代,或者对原有组件进行包装,甚至是自己实现一个 Select 组件。

示例页面:assweeecan.github.io/antd_perfor…

性能测试图:

<img src="https://pic2.zhimg.com/v2-b1e263ab1f1016c5ead864422bec6ef1_b.png" data-rawwidth="745" data-rawheight="963" class="origin_image zh-lightbox-thumb" width="745" data-original="https://pic2.zhimg.com/v2-b1e263ab1f1016c5ead864422bec6ef1_r.png">

可以看到打印时间从 50ms 降低到 10ms ,用户输入不会再感受到卡顿。 这里笔者对 Select 组件进行了一层包装,将原来由 Children 传入 Option 的方式改为传入一个 Option 数组,这样就能更好的控制 Select 组件的更新以达到提高性能的目的。 包装 Select 组件示例:github.com/assweeecan/…

当然我们还可以将 Select 组件替换为 Modal + Checkbox 的组合,这样用户就可以获得更大的视野,性能也会提升。

总结

因为 React 有一种将更新范围扩大的特点,因此有些时候会遇到性能瓶颈,特别是在表单里。当我们使用 antd 遇到性能瓶颈的时候,可以在这方面进行优化。 以上的优化都是在不修改原有界面的情况下进行优化。然而要想彻底解决这些性能问题,就应该从设计入手,减少表单域的数量,对选项进行分组和折叠,又或者使用弹窗显示一组复杂的表单域,尽可能设置默认值,才能从根本上解决性能问题,也会减少用户的工作量,获得更好的用户体验。