性能调优:让十万级数据跑满帧
deck.gl 慢,绝大多数时候不是 GPU 画不动,而是 JavaScript 在每帧重复做无用功。 先定位瓶颈,再决定是减数据、减重算,还是减拾取。
四个瓶颈,对四种症状
| 症状 | 瓶颈 | 优先手段 |
|---|---|---|
| 拖动时卡,松开就顺 | 拾取或访问器重算压在 CPU 上 | 关 pickable、加 updateTriggers、节流悬停 |
| 点很多时整体掉帧,缩放到局部变好 | 片元填充率被打满 | 减小半径、限制 radiusMaxPixels、聚合替代逐点 |
| 每帧都掉,且与图层数量相关 | 每帧重建图层对象 | 固定 id、缓存图层实例、只在必要时 setProps |
| 数据一更新就卡顿一下 | 数组重建与属性拷贝 | 复用数组、就地修改、分块更新 |
先量,再改
不测就调是盲调。用 requestAnimationFrame 统计一秒内的回调次数,
是最简单也最接近用户感受的帧率指标。
var frames = 0, last = performance.now(), fps = 0;
function tick(now) {
frames++;
if (now - last >= 1000) {
fps = Math.round(frames * 1000 / (now - last));
frames = 0;
last = now;
label.textContent = fps + ' fps';
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
只看 fps 还不够,要配合数据量和视角一起记录:同一份数据,全览视角和 贴地视角的负载差别可能有数倍。
图层 id 与对象复用
每次 setProps({layers}),deck.gl 会按 id 匹配旧图层。
同 id、同数据引用时它直接复用已有缓冲;一旦缺 id,它会销毁重建,
顶点缓冲、纹理、聚合结果全部重来。
/* 差:每次调用都新建,且没有 id */
deckgl.setProps({
layers: [new deck.ScatterplotLayer({ data: data, getPosition: getPos })]
});
/* 好:图层实例缓存,只换数据引用 */
var pointsLayer = new deck.ScatterplotLayer({
id: 'points',
data: data,
getPosition: getPos
});
deckgl.setProps({ layers: [pointsLayer] });
数据引用不变,就别新建数组
实时数据最常见的写法是每秒 data = data.concat(newItems)。
这会产生新数组,deck.gl 认为数据已变,重新上传全部顶点。
增量场景应该就地写:预先分配定长数组,用 data.length = n
或写入固定槽位,再配合 updateTriggers 触发局部更新。
如果业务允许,把坐标在数据准备阶段算好,访问器只做一次属性读取:
/* 预处理一次 */
raw.forEach(function (d) {
d.position = [d.lng, d.lat];
d.size = Math.sqrt(d.flow) * 2;
});
var layer = new deck.ScatterplotLayer({
id: 'points',
data: raw,
getPosition: function (d) { return d.position; },
getRadius: function (d) { return d.size; },
radiusUnits: 'pixels'
});
这仍然是每帧调用函数,但避免了每次求值都创建数组对象, 在十万级数据上通常能省下可观的时间。
拾取的代价
pickable: true 的图层会额外渲染一张拾取缓冲。
图层多、要素多时,鼠标移动触发的拾取会把 CPU 和 GPU 一起拖住。
三种做法按代价从低到高:
| 做法 | 适用 | 代价 |
|---|---|---|
只在需要时开 pickable | 静态展示页 | 无 |
| 节流悬停,100ms 一次 | 需要提示但不需要实时 | 轻微延迟 |
用 pickObject 手动拾取 | 点击时精确查询 | 每次调用一张拾取缓冲 |
拾取半径也会影响开销。radiusUnits: 'meters' 时,缩放到低层级,
每个点在地面的覆盖范围变大,命中测试的成本同步上升。
把 radiusMinPixels 设成 1–2,既能看见,也能限制过大的覆盖。
用聚合换数量
百万级点逐点绘制时,真正的问题往往不是顶点数,而是半透明重叠造成的
填充率爆炸。此时换成 HexagonLayer 或 GridLayer,
画的是几千个单元,视觉信息反而更清楚。
| 数据量 | 推荐方案 |
|---|---|
| 一万以内 | 任意单要素图层,无需优化 |
| 十万级 | ScatterplotLayer + 像素半径 + 关闭描边 |
| 百万级 | 聚合图层,或分块 + TileLayer |
| 千万级 | 预聚合、二进制数据、服务端切片,前端只画结果 |
设备像素比
useDevicePixels: true 在高分屏上按 2 倍或 3 倍分辨率渲染,
画面锐利,但片元数量翻倍。数据密集时把它设成 false,
或限制在 1.5,是最快见效的一档开关。
与之相关的是画布尺寸。用 canvas 初始化时,宽度高度必须显式给定;
如果容器是弹性布局,监听 resize 后调用 setProps
同步尺寸,否则画布会被拉伸,视觉上模糊且多耗填充。
别在渲染循环里创建对象
动画驱动 setProps 时,每次传入的 layers 数组、
颜色数组、配置对象都应该是复用的。下面这种写法每秒会产生上百个临时数组,
触发 GC 抖动,表现为规律性的卡顿:
/* 每帧新建颜色数组,不推荐 */
function frame(t) {
deckgl.setProps({
layers: [new deck.ScatterplotLayer({
id: 'points',
data: data,
getFillColor: [Math.sin(t) * 100 + 120, 80, 200]
})]
});
}
正确做法是保持图层实例,只改可变属性:
pointsLayer.props.getFillColor 这类内部字段不要直接改,
而应把颜色放进闭包变量,并用 updateTriggers 声明依赖,
然后 setProps({layers: [pointsLayer]})。
调优顺序建议:先确认是否每帧重建图层,再确认是否每帧新建数据, 然后关掉不必要的拾取与描边,最后才考虑减数据量。 前两步的收益通常远大于换图层类型。
经纬志(2026)。《性能调优:让十万级数据跑满帧》。https://www.jingweizhi.com/deckgl/tutorial/performance.html(访问日期 2026-11-03)。