经纬志
首页 / deck.gl / 教程

性能调优:让十万级数据跑满帧

先看结论

deck.gl 慢,绝大多数时候不是 GPU 画不动,而是 JavaScript 在每帧重复做无用功。 先定位瓶颈,再决定是减数据、减重算,还是减拾取。

最后验证 2026-11-03 · deck.gl 9.0 · 更新 2026-10-27 · 引用本文

四个瓶颈,对四种症状

症状瓶颈优先手段
拖动时卡,松开就顺拾取或访问器重算压在 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)。