认识 LTTB
2025-05-28 22:30
“真正的问题,往往藏在你以为的理所当然之外。”
在这个项目里,我第一次正面面对了 十几万条数据同时渲染到首页 的挑战。
起点:Tick 数据拖垮首页性能
我们正在开发一款外汇类 App,基于 React Native 构建。首页是一个信息密集的长列表,用 FlatList
渲染各种模块和卡片,大多数情况下表现良好。
但其中有一项叫做「Tick 交易图表」的组件,用来展示某个交易品种的 Tick 数据。这类数据是 毫秒级的交易记录,数量少时只有几十条,多时能达到十几万条。
后端坚持:“必须是原始数据,不能裁剪。”
(我猜,也许是因为懒)
结果很明显:
只要图表涉及的数据达到几千条以上,哪怕组件设计再轻量,滚动时仍然明显卡顿;滑动过程中 FPS 降低、页面响应延迟,体验极差。
我尝试了很多优化手段:
- 图表懒加载、局部渲染
- 限制图表首屏渲染条数
- 组件虚拟化、延迟绘制
虽然缓解了一些问题,但没有解决根本矛盾 —— 数据量太大,渲染压力太重。
转折点:偶然遇见 LTTB
在一次搜索 “如何高效展示大规模时间序列数据” 的过程中,我偶然看到一个关键词:
LTTB(Largest-Triangle-Three-Buckets)算法。
我点进去看了一下,这东西的原理立刻让我眼前一亮。
LTTB 是什么?
LTTB 是一种为可视化而设计的下采样算法,它在不破坏数据趋势的前提下,将大量点压缩为少量代表性点。
核心思想如下:
- 保留原始数据的首尾点;
- 把中间的数据分成 N 个桶(bucket);
- 每个桶中选出一个最“关键”的点,依据是它与前一个和后一个桶代表点形成的三角形面积最大。
因此,LTTB 不简单平均、也不等间隔丢弃,而是保留那些最能反映走势变化的数据点。
比如你原始数据有 100,000 条,LTTB 可以压缩为 200 条,但这 200 条会最大程度保留原数据的趋势。
实践:在 Tick 图中应用 LTTB
了解原理后,我立刻尝试将其应用到我们的问题中。考虑到已有的开源实现大多功能冗余或不够灵活,我决定基于原理自己实现一个精简版本。
原始 Tick 数据结构如下:
TS[{ time: 1716892458000, ask: 1.0765, bid: 1.0765 }, ...]
为了配合图表组件(如 victory-native
),我在渲染前对数据进行 LTTB 压缩:
TSimport lttb from 'js-lttb' const lttbData = data.dukaTicks.length > 200 ? lttb(data.dukaTicks, 200, 'time', 'ask') // 从十几万压缩到 200 : data.dukaTicks;
压缩之后,传给图表组件的数据从十几万条缩减到了几百条,页面响应速度和滑动流畅度显著提升,而视觉趋势几乎没有肉眼可见的损失。
附上照片图片加载中...
成果沉淀:发布 js-lttb
在项目落地后,我将这个算法封装成一个轻量级的 NPM 包,发布为:
特点:
- 零依赖,纯函数实现;
- 支持任意结构数据,自定义
x
和y
映射字段; - 支持时间戳、数字等多类型数据;
- 体积极小(压缩后 < 2KB),非常适合前端场景;
安装使用:
BASHnpm install js-lttb
TSimport lttb from 'js-lttb' const simplified = lttb(data, 500, 'timestamp', 'value')
写在最后
这个过程让我意识到一个重要的认知误区:
前端并不只是堆 UI、调样式,算法思维同样重要。
很多性能瓶颈表面看是“组件卡”、“页面慢”,但真正的问题可能在于数据处理方式 —— 特别是在与后端无法妥协的前提下,如何用前端手段“聪明”地解决问题,就成了关键。
并不是所有性能问题都得靠“强制压缩”或者“懒加载”来处理。
算法的力量,有时候就藏在我们不知道的角落里,也许这正是“成长”的一部分吧 —— 去找到那些我们之前从未听过,却正是所需的答案。
如果你也遇到了 Tick 数据图表相关的性能问题,欢迎试试 js-lttb。如果有改进建议或想法,也欢迎交流与分享。