Ant Design Statistic组件深度实践与常见问题解决方案
一个数据大屏项目的意外需求
最近做的那个数据大屏项目,本来以为就是常规的数据展示,结果产品突然提了个需求:要在大屏幕上显示一些关键的统计数据,比如实时用户数、交易金额、订单量这些,而且要求要有动画效果,数字要从0滚到目标值。
开始没想到这个看起来简单的功能会有这么多坑,之前也没专门研究过Statistic组件,就想着用现成的UI库吧,Ant Design的Statistic看起来不错,就开始用了。
基础实现倒是挺简单
最开始的代码很基础:
import { Statistic } from 'antd';
import CountUp from 'react-countup';
// 简单的计数器组件
const SimpleCounter = ({ title, value, prefix, suffix }) => {
return (
<div className="stat-item">
<div className="title">{title}</div>
<CountUp
start={0}
end={value}
duration={2}
prefix={prefix || ''}
suffix={suffix || ''}
separator=","
>
{({ countUpRef }) => (
<div className="value" ref={countUpRef} />
)}
</CountUp>
</div>
);
};
// 或者直接用Antd的Statistic
const AntdCounter = ({ title, value }) => {
return (
<Statistic
title={title}
value={value}
formatter={(val) => val.toLocaleString()}
/>
);
};
基础功能很快实现了,但项目中遇到了几个问题,特别是性能方面的问题。
最大的坑:批量更新时的性能问题
项目中有个需求是要同时更新多个统计数据,每隔几秒刷新一次,刚开始测试的时候只有3-4个指标,感觉还行,但后来加到了10多个,页面就开始卡顿了。
开始没想到问题出在哪里,折腾了半天才发现是每次更新都会触发所有的CountUp动画,而且是在同一个时间点触发,浏览器一下要处理十几个动画,CPU占用直接飙升到70%+。
后来调整了方案,加了防抖和分批更新:
import { useState, useEffect, useCallback } from 'react';
import { debounce } from 'lodash';
const BatchStats = () => {
const [stats, setStats] = useState([
{ id: 1, title: '用户数', value: 0 },
{ id: 2, title: '订单数', value: 0 },
{ id: 3, title: '收入', value: 0 }
]);
// 防抖更新函数
const debouncedUpdate = useCallback(
debounce((newStats) => {
setStats(prev => prev.map((stat, index) => ({
...stat,
value: newStats[index]?.value || stat.value
})));
}, 300),
[]
);
// 分批延迟更新,避免同时触发所有动画
const updateStatsWithDelay = (newStats) => {
newStats.forEach((stat, index) => {
setTimeout(() => {
setStats(prev => prev.map(p => p.id === stat.id ? { ...p, value: stat.value } : p));
}, index * 100); // 每个间隔100ms
});
};
useEffect(() => {
const fetchStats = async () => {
try {
const response = await fetch('https://jztheme.com/api/stats');
const data = await response.json();
updateStatsWithDelay(data);
} catch (error) {
console.error('Failed to fetch stats:', error);
}
};
fetchStats();
const interval = setInterval(fetchStats, 5000);
return () => clearInterval(interval);
}, []);
return (
<div className="stats-container">
{stats.map(stat => (
<div key={stat.id} className="stat-card">
<div className="stat-title">{stat.title}</div>
<CountUp
start={0}
end={stat.value}
duration={1.5}
separator=","
preserveValue={true} // 保持当前值,避免重置
/>
</div>
))}
</div>
);
};
这里要注意我踩过好几次坑的地方:preserveValue属性很重要,不然每次父组件重新渲染,动画都会从0开始。还有就是分批延迟更新,虽然用户体验稍微差一点(不是同时更新),但是性能好了很多。
另一个坑:数字格式化和精度问题
数据大屏里经常要显示很大的数字,比如交易金额可能达到百万级别,这时候需要做格式化处理。但项目中遇到了精度问题,特别是在处理浮点数的时候。
const formatNumber = (num, decimals = 2) => {
if (typeof num !== 'number') return num;
// 处理科学计数法
const formatted = Number(num).toLocaleString('en-US', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
return formatted;
};
// 自定义的Statistic组件
const CustomStat = ({ title, value, unit = '', type = 'number' }) => {
const [displayValue, setDisplayValue] = useState(0);
useEffect(() => {
// 使用缓动动画,而不是线性动画
let start = displayValue;
const end = value;
const duration = 1500; // 1.5秒
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
// 缓动函数
const easeProgress = 1 - Math.pow(1 - progress, 3);
const currentValue = start + (end - start) * easeProgress;
setDisplayValue(currentValue);
if (progress < 1) {
requestAnimationFrame(animate);
} else {
setDisplayValue(end); // 最终设置精确值
}
};
animate();
}, [value]);
return (
<div className="custom-stat">
<div className="stat-label">{title}</div>
<div className="stat-value">
{formatNumber(displayValue, type === 'money' ? 2 : 0)}
{unit && <span className="unit">{unit}</span>}
</div>
</div>
);
};
这里折腾了半天发现,如果用第三方的动画库,有时候会因为精度问题导致显示的数字不够准确,特别是在大数据场景下。所以最后还是自己实现了简单的动画逻辑,这样控制起来更灵活。
CSS样式也有些小麻烦
数据大屏对视觉效果要求比较高,光有动画还不够,样式也要够炫。但Statistic组件默认的样式比较基础,需要大量的自定义。
.stats-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
padding: 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
padding: 20px;
color: white;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-title {
font-size: 14px;
opacity: 0.8;
margin-bottom: 10px;
}
.stat-value {
font-size: 28px;
font-weight: bold;
text-align: center;
animation: glow 2s ease-in-out infinite alternate;
}
@keyframes glow {
from { text-shadow: 0 0 5px #fff; }
to { text-shadow: 0 0 20px #fff, 0 0 30px #e60073; }
}
.unit {
font-size: 16px;
margin-left: 5px;
}
这里的CSS动画效果看起来不错,但在低端设备上可能会有性能问题,所以实际部署的时候还需要根据设备性能动态调整。
最终效果和遗留问题
经过一番折腾,最终的效果还算满意。数据显示比较流畅,动画效果也达到了预期。CPU占用基本维持在20%-30%左右,比最初的一下子70%好多了。
不过还是有些小问题没有完美解决:比如在移动设备上偶尔还是会有点卡顿,还有就是当数据变化特别大的时候(比如从几万直接跳到几十万),动画时间会显得很长。这些问题暂时影响不大,就没继续深究了。
这个组件最后在项目中复用性还挺高的,封装成了一个通用的数据展示组件,后面其他模块也有用到。
回顾与反思
这次的经验告诉我,看似简单的数字统计功能,实际落地的时候还是要考虑很多因素:性能、用户体验、兼容性等等。特别是对于数据大屏这种场景,用户的容忍度比较低,任何卡顿都会很明显。
以后再遇到类似的需求,我会提前做好性能测试,特别是在数据量比较大的情况下。另外也会考虑要不要做一些降级处理,比如移动端或者性能较差的设备上关闭复杂的动画效果。
以上是我个人对这个Statistic组件应用的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,比如结合WebSocket实时推送数据等,后续会继续分享这类博客。

暂无评论