必须用 Canvas/WebGL 批量绘制替代 DOM Marker:高德用 CanvasLayer,百度用 CustomLayer,Leaflet 用 VectorGrid/glify,deck.gl 最灵活;需服务端分片、Worker 解析、空间索引与视口更新。

HTML5 地图上一次性渲染百万级点标记(Marker)必然卡死,这不是优化问题,而是浏览器渲染模型的硬限制——你不能靠“调优”绕过 DOM 节点数量与 Canvas 绘制能力的物理瓶颈。
为什么直接加百万 Marker 必然卡?
主流地图 SDK(如高德、百度、腾讯、Leaflet + L.Marker)默认为每个点创建独立 DOM 元素或 Canvas 图形对象。百万级节点会触发:
- DOM 树膨胀至数百万节点(含事件监听器、样式计算、重排重绘),Chrome 通常在 10 万级就明显掉帧
- GPU 内存溢出或浏览器主动 kill 渲染进程(尤其移动端)
- 地图 SDK 自身的碰撞检测、聚合逻辑在未降级时呈 O(n²) 复杂度
必须用 canvas 或 WebGL 图层替代 DOM 标记
真实可行路径只有一条:放弃逐点创建 Marker,改用底层绘制接口批量处理。不同 SDK 方案差异大:
- 高德地图:用
AMap.CanvasLayer,自己在canvas上用ctx.fillRect()或ctx.drawImage()画点,配合map.getPixelFromLngLat()做坐标转换 -
百度地图:用
BMapGL.CustomLayer(需开启 WebGL 模式),传入draw回调,在gl上用着色器绘制点精灵(point sprite) - Leaflet:用
Leaflet.VectorGrid或Leaflet.glify,把点数据转成 GeoJSON + 着色器,交由 GPU 批量绘制 - 纯
WebGL方案(如 deck.gl +ScatterplotLayer):最灵活,支持千万级点,但需自行管理地图投影与缩放同步
数据必须预处理:不是“加载”,而是“分片 + 动态加载”
即使用了 WebGL,百万点原始数据(如 10MB GeoJSON)也会阻塞主线程解析。关键动作是:
立即学习“前端免费学习笔记(深入)”;
- 服务端按空间格网(如 Geohash 或 quadkey)切片,前端只请求当前视口 + 缓冲区内的分片
- 前端用
Worker解析 GeoJSON,避免 JS 主线程冻结 - 缩放层级变化时,销毁旧图层、触发新分片请求,而非“叠加”所有点
- 若点有属性(如温度、类别),用
Uint8Array或Float32Array存储数值,避免对象数组内存爆炸
容易被忽略的性能断点:坐标转换与交互反馈
很多人以为画出来就完了,但实际卡顿常发生在交互环节:
-
canvas/WebGL图层默认无原生点击事件,要实现“点击某点”得自己做射线拾取(map.lngLatToContainerPoint→ 反向查最近点),复杂度从 O(1) 变成 O(n),必须建k-d tree或quadtree索引 - 缩放时若每帧都重算全部点像素坐标,CPU 直接拉满——应只更新视口内点,且用
requestAnimationFrame节流 - 高 DPI 屏幕(如 MacBook Retina)下,
canvas尺寸需乘以window.devicePixelRatio,否则点模糊或错位,但内存占用翻 4 倍
真正跑通百万点不卡,核心不在“怎么画”,而在“怎么不画不该画的”和“怎么让 CPU/GPU 各干各的活”。数据结构、线程分工、GPU 绘制粒度,三者缺一不可。











