唯杰地图 logo唯杰地图
VJMAP
EN

render-buckets

vjmapext/references/perf/render-buckets.md 6.5 KB106 行
原文

本文回答:vjmapext 渲染桶(hot / cold / dynamic)原理 + 实体如何进哪个桶。

一、什么是渲染桶

MapCadLayer 内部把实体按状态分成 3 个桶——每个桶是一条独立的 GeoJSON source(mapcad-hot / mapcad-cold / mapcad-dynamic),样式表按 _active 属性区分高亮。feature properties 里的实体回链键是 _entity_id(另有 _entity_type/_role/_meta),调试用 map.getSource('mapcad-cold')._data.features 直读比 querySourceFeatures(走瓦片管线,异步生成)更稳:

桶 何时进 特点
cold(冷) 普通实体:未选中、未高亮、非 dynamic 绝大多数实体在这;桶脏时整桶 setData 重传
hot(热) 被选中(isSelected)或高亮(isHighlighted)的实体 桶很小,选中态单独走 _active 高亮样式
dynamic(动态) 设了 entity.dynamic = true(且未选中) 专为高频变化实体准备的小桶

注意:三桶都是 GeoJSON source,没有 DYNAMIC_DRAW 级区别(DYNAMIC_DRAW 是 FX instancing 的内部细节);dynamic 的收益是"小桶重传便宜",不是特殊 buffer 类型。

二、流转规则

每次增量更新(_incrementalUpdate)都按同一条规则重新分类:

store.isSelected(id) || store.isHighlighted(id)  →  hot
否则 entity.dynamic === true                     →  dyn
否则                                             →  cold
  • addEntity → 默认进 cold(除非一进来就被选中 / dynamic)
  • 选中实体 → 升入 hot;取消选中 → 降回 cold / dyn
  • changed() 的实体 → 从原桶摘除、重新编码、按上表重新分桶;所在桶标脏 → 下一帧该桶 setData 重传
  • styleChanged() 走 updateStyleProps 快路径:只重算样式属性,不重编码几何(桶照样标脏重传)

没有"hot 静置 N 秒后自动降级"机制——hot 完全由选中/高亮状态决定,取消选中即离开。

三、什么时候用 dynamic

当实体每帧都要变位置 / 大小(比如飞线、闪烁、路径跟随):

const poly = new vjmapext.PolylineEnt(pts);
poly.dynamic = true;
mapcadLayer.addEntity(poly);

setInterval(() => {
  const vs = newPoints();
  poly.bulgePoints = vs.map(([x, y]) => ({ point: new vjmapext.Point2D(x, y), bulge: 0 }));
  poly.changed();           // 高效:只改 dynamic buffer
}, 16);

如果不设 dynamic,cold 桶每次都会整桶 rebuild,1 万实体下一帧卡死。

四、hot 桶 = 选中/高亮桶

isSelected || isHighlighted 为 true 的实体进 hot 桶,feature 打上 _active = "true",由样式表里的 _active 过滤器渲染成高亮样式。

  • 用户 click 选中 → 实体升入 hot,立刻显示选中态
  • 取消选中 / store.setSelection([]) → 降回 cold(或 dyn)
  • hot 桶始终很小 → 选中态变化的重传代价极低

选中几百上千实体时 hot 桶同样会变大——超大选集的高亮渲染成本与选集大小成正比。

五、查看当前桶分布(调试)

renderPipeline 无公开的桶统计 API;可在浏览器 devtools 里查看实例的 _hotFeatures / _coldFeatures / _dynFeatures 集合大小(私有字段,仅供调试,勿依赖)。

如果 hot 一直很大(上千)→ 意味着有"隐形"反复变化的实体,需排查。

六、性能对比(测试数字仅供参考)

场景 cold 桶 hot 桶 dynamic 桶
1 万实体静态显示 60 FPS - -
加 1 条新实体 60 → 59 FPS(rebuild 一次) 60 FPS 60 FPS
每帧改 100 条颜色 30 FPS(每帧 rebuild) 50 FPS 60 FPS
每帧改 100 条几何 5 FPS 20 FPS 58 FPS

七、什么时候不要设 dynamic

  • 实体只是偶尔变(比如用户点一次改一次颜色)→ 默认就好
  • 实体数 < 100 且都会动 → 默认 cold 也能 60 FPS

八、cold 桶批量加的最优做法

// 正确:直接循环——addEntity 只标脏,重绘按 requestAnimationFrame 合并成一帧
for (const p of data) mapcadLayer.addEntity(toEnt(p));

// 注意:不要在循环里穿插 await / 异步,会把添加摊到多个帧

详见 batch-rendering 和 batch/entity-bulk-add。

九、常见坑

  1. 期望实体高频变化但没设 dynamic → 每帧 rebuild 整个 cold buffer,几千实体下 FPS 断崖。
  2. 所有实体都设 dynamic → dynamic 桶膨胀,每帧都要走完整编码路径,反而不如 cold。
  3. 忘记 deleteEntity 的动态实体:实体一直在 store 和 dyn 桶里,每帧随 dyn source setData 上传;不存在"slot 溢出",纯粹是白白吃内存和带宽。
  4. 标脏后画面不更新:确认确实调了 changed()/styleChanged();只改字段不标脏不会触发重绘。
  5. dynamic + styleChanged:同样有效且更省——updateStyleProps 原地改缓存 feature 的样式属性,连 toGeoJSON 重编码都跳过;只改样式时优先用它而不是 changed()。
  6. 一个"毒实体"拖死整条管线:store 里任一实体的点字段被赋 {x,y}/数组字面量(而非 Point2D 实例)时,_fullRebuild → renderCache.getOrCompute → toGeoJSON/clone 抛 clone is not a function,整帧渲染中断——三个桶全部清空,且页面无业务层报错(异常发生在 rAF 回调里,仅见 pageerror)。更糟的是 store.delete(id) 也会崩:undoManager.recordDelete → entity.clone() 同样抛错,毒实体连正常删除都走不通。应急清除:先 store._undoManager = null 绕过 undo 快照再 store.delete(id)(删完恢复引用),或定位后整体 fromJSON 重建。
  7. 挂载时机:addControl 要么在 map 'load' 之前,要么在 map.loaded() 为真时(官方示例是 await map.onLoad() 后紧接着挂,这两种都安全)。onLoad 之后又等了别的异步、地图正加载新瓦片时才挂,loaded() 为 false 而 'load' 不再发 → onAdd 的 loaded()?doConnect():on('load',doConnect) 两路落空 → 桶永远不建、画面静默空。补救:map.fire('load') 重发事件。另注意 removeControl 会 removeSourcesAndLayers 拆掉共享的 mapcad-* 桶——同一 map 挂第二个实例再移除,会把第一个的桶也清掉且不重建。

相关