多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

20万级地图数据不卡顿:Canvas渲染与动画优化实战

20万级地图数据不卡顿:Canvas渲染与动画优化实战 做地图可视化的朋友迟早都会碰到一个坎地图上要撒20万个点页面直接卡成幻灯片好不容易渲染出来了想加点动画效果又开始掉帧、闪烁、甚至白屏。这篇内容就是专门聊这个坎怎么过聊聊20万级数据的地图渲染怎么优化不卡以及地图动画怎么做才流畅。我会用实际做过的方案、实测过的代码和踩过的坑来讲适合正在用Leaflet、OpenLayers、Mapbox GL等地图库做数据可视化、但还没摸清性能瓶颈在哪的开发者参考。先说结论20w数据能不能跑得动不是看地图库而是看你的渲染方案。方案选对了二十万点不仅不卡动画还能稳定在60帧附近。下面把思路、步骤和排查经验一条条拆开。1. 为什么20w数据会让地图直接卡死1.1 性能瓶颈不在GPU而在DOM很多人一开始的做法是给每个数据点创建一个地图Marker比如用Leaflet的L.circleMarker或者Mapbox的divIcon。点少的时候没感觉但点一旦上万问题立刻就暴露出来。每个Marker本质上都是一个独立的DOM元素。20万个DOM节点意味着浏览器要同时管理20万个元素的创建、定位、样式计算和重绘。这里有个容易被忽略的细节地图每次拖动或者缩放的时候这些DOM元素的位置都要重新计算一遍而浏览器计算布局的开销是和元素数量呈线性甚至超线性增长的。我实测过一组数据5000个divIcon的点地图拖动时FPS还能维持在40到50左右到了2万个点FPS直接掉到个位数到10万以上基本上等于是PPT播放。所以只要走了“一个点一个DOM”这条路不管用什么地图库20w这条线都过不去。1.2 另一个隐性瓶颈数据源格式和解析除了DOM渲染压力还有一个很容易忽略的地方数据本身的解析过程。假设你拿到的是一个20万条的JSON数组每条包含经纬度和属性字段。地图库在渲染之前往往需要自己再做一次数据解析、坐标转换或者属性读取这个过程的耗时在大数据量下同样非常可观。如果你把20万条JSON直接塞给地图库的source尤其是那些内部还要做空间索引、投影转换的库光是数据进source这一步就可能卡上十几秒。这不是库写得差而是JSON本身的解析效率就摆在那里500KB的数据还好5MB以上的数据一次性解析主线程基本就冻结了。所以数据预处理在整个链路里占的重要性可能比你想象中还要高。1.3 真正能抗住20w数据的方案是Canvas渲染业内目前能稳定处理20w以上点数据的地图方案核心思路基本都是同一套把点数据画到Canvas上而不是创建DOM元素。Leaflet有官方插件Leaflet.Canvas可以自定义Canvas图层。Mapbox GL本身就支持Canvas图层和自定义Layer。OpenLayers自带Canvas renderer也可以直接渲染矢量数据。更重型一点的方案比如deck.gl这种基于WebGL的图层方案适合点数据量大到离谱的场景。用Canvas做图层核心思路是地图移动时不再更新20万个DOM的位置而是只更新一张画布的位置然后重新绘制画布里的内容。浏览器的重绘压力从“20万次DOM更新”降到了“一次Canvas绘制”性能差距是数量级的。至于WebGL和Canvas 2D的取舍我的经验是20万这个量级Canvas 2D完全够用如果你以后要上百万甚至千万级数据再考虑WebGL也不迟。Canvas 2D的好处是上手简单、调试方便、兼容性好不需要处理着色器、缓冲区这些复杂概念。这篇文章下面的实现也以Canvas 2D为主线WebGL思路会顺带提一下。2. 渲染方案选型不是所有地图库都适合硬塞20w数据2.1 Leaflet Canvas图层轻量项目首选Leaflet本身是轻量地图库里的常青树它的插件生态里有一个Leaflet.Canvas可以让你把一个自定义的Canvas画布覆盖在地图上。用这个插件的方式很简单let canvasLayer L.canvasLayer({ resolution: 2 }).addTo(map); canvasLayer.drawCanvas function (canvas, bounds) { let ctx canvas.getContext(2d); ctx.clearRect(0, 0, canvas.width, canvas.height); // 遍历数据用投影坐标换算后绘制 data.forEach(function (point) { let latlng L.latLng(point.lat, point.lng); let pixel map.latLngToContainerPoint(latlng); ctx.beginPath(); ctx.arc(pixel.x, pixel.y, 2, 0, Math.PI * 2); ctx.fill(); }); };如果你只是想快速交付一个20w点展示的demo这个方案是上手最快的。需要注意的是绘制的时候不能每次把20w数据全量重画后续我会说怎么分块处理。2.2 Mapbox GL适合同时又要交互又要动画的项目Mapbox GL内置的是WebGL渲染管线性能上限比Canvas 2D高不少。如果项目里除了20w点还要做轨迹动画、热力图过渡、图层开关这类效果Mapbox GL是更省心的选择。map.addLayer({ id: scatter, type: circle, source: { type: geojson, data: geojsonData }, paint: { circle-radius: 3, circle-color: #ff6600 } });但注意一点Mapbox GL的GeoJSON source在数据量非常大的时候初始解析仍然会有明显耗时。我的习惯是用addSource之前先做一次数据抽稀或者属性精简只保留渲染必需字段。2.3 自研Canvas图层方案完全可控但工作量大如果项目要求完全自定义视觉表现比如点要按业务字段放大缩小、要叠加复杂动画、要精确控制帧率自研一个Canvas图层是最彻底的方案。大体框架是在地图容器上覆盖一个全屏Canvas监听地图的move、zoom事件事件触发时拿最新的地图bounds算出当前视野内有哪些点再绘制到Canvas上。只要绘制逻辑本身足够快配合requestAnimationFrame做节流就能获得流畅的体验。这个方案不依赖具体地图库的渲染能力缺点是你得自己处理经纬度到屏幕坐标的转换、缩放级别变化时的点大小调整、以及Canvas尺寸和DPR的关系。但一旦搭好骨架地基非常稳后面想加什么效果都方便。下文的主要实操讲解也围绕这个方案展开。3. 20w数据渲染实操从数据预处理到Canvas绘制3.1 数据预处理先把不需要的字段砍掉拿到20w原始数据后不要直接塞进渲染逻辑。第一件事是精简字段。比如原始数据可能是这样的[ { name: 点位1, lat: 30.123456, lng: 120.654321, category: A, customId: u_10001, createTime: 2024-01-01 12:00:00, score: 98.6, ext: { ... } } ]但渲染地图点只需要lat、lng顶多再加一个颜色分类字段。其余字段全部可以留到用户点击查看详情的时候再按ID去查。这样一条数据从几百字节压缩到十几个字节20w条的总量就能从几十MB降到几MB。这个步骤看着不起眼但影响很大。数据量小了后续所有遍历、序列化、传输的耗时都会同步降下来。我见过有人直接把后端给的一整包数据塞进去结果光是JSON.parse就花了好几秒。字段精简之后同样的数据量解析时间能降到十分之一。3.2 用四叉树/网格索引裁剪视野内数据Canvas绘制20w个点如果每次都遍历全部数据画一遍也是会卡的。实际上大部分点根本不在当前视野里画了也白画。所以要做一个空间索引按当前地图bounds快速筛选出视野内的数据点。单机方案里最常见的是四叉树Quadtree它把地图范围按四象限递归分割每个叶子节点装一批点。查询时只需要遍历和当前bounds相交的叶子节点不需要全量扫描。如果你不想引入复杂的四叉树实现还有一个更取巧的办法网格索引。把经纬度范围划分成固定大小的网格比如每0.01度一格每个格子内存放数据点的索引。视野查询时计算当前bounds覆盖了哪些格子直接取出这些格子里面的数据遍历。这个方法实现比四叉树简单很多性能足够应对20w这个量级。class GridIndex { constructor(data, gridSize 0.01) { this.grid new Map(); this.gridSize gridSize; data.forEach((point, index) { let key this._getKey(point.lng, point.lat); if (!this.grid.has(key)) this.grid.set(key, []); this.grid.get(key).push(index); }); } _getKey(lng, lat) { let col Math.floor(lng / this.gridSize); let row Math.floor(lat / this.gridSize); return ${col}_${row}; } query(bounds) { let result []; let minCol Math.floor(bounds.getWest() / this.gridSize); let maxCol Math.floor(bounds.getEast() / this.gridSize); let minRow Math.floor(bounds.getSouth() / this.gridSize); let maxRow Math.floor(bounds.getNorth() / this.gridSize); for (let c minCol; c maxCol; c) { for (let r minRow; r maxRow; r) { let key ${c}_${r}; if (this.grid.has(key)) { result.push(...this.grid.get(key)); } } } return result; } }构建这个索引本身需要遍历一次数据20w条大概几十毫秒完全可接受。3.3 Canvas绘制细节离屏Canvas和视野裁剪找到当前视野内的点之后就可以绘制了。但这里有一个很影响性能的小细节不要在地图的move事件回调里直接清屏重绘而是先用一张离屏Canvas把静态点画好主Canvas在动画帧里再drawImage这张离屏Canvas。具体说就是地图移动时先把当前视野内的点画到一张隐藏的Canvas上。地图停止移动后把这张离屏Canvas整体贴到屏幕Canvas上。如果有动画效果动画逻辑单独在每一帧里操作屏幕Canvas不去干扰静态点图层。这么做的好处是地图在拖动过程中只需要以很低频率刷新离屏Canvas而屏幕层的渲染可以用requestAnimationFrame去控制不会每触发一个move事件就同步执行一次20w数据级的绘制。绘制单个点的时候用ctx.fillRect比ctx.arc更高效因为arc涉及路径计算和弧度运算。对于圆形点可以用ctx.arc画好一个放到离屏小画布里然后每次drawImage这个成品点图性能能提升不少。如果点形状就是简单的圆形甚至可以直接用ctx.fillRect配合正方形像素点来表示效果上差别不大性能上差距明显。3.4 渲染主循环的代码骨架用一个最小可运行的自研Canvas图层来做示范地图库以Leaflet为例但思路适用于所有地图库const overlay L.layerGroup().addTo(map); const canvas L.DomUtil.create(canvas, data-layer); const ctx canvas.getContext(2d); canvas.style.position absolute; canvas.style.pointerEvents none; L.DomUtil.addClass(canvas, leaflet-zoom-animated); overlay.addLayer({ onAdd: function () { map.getPanes().overlayPane.appendChild(canvas); return this; }, onRemove: function () { canvas.remove(); return this; } }); function draw() { let bounds map.getBounds(); let topLeft map.latLngToContainerPoint(bounds.getNorthWest()); let bottomRight map.latLngToContainerPoint(bounds.getSouthEast()); canvas.width window.devicePixelRatio * (bottomRight.x - topLeft.x); canvas.height window.devicePixelRatio * (bottomRight.y - topLeft.y); canvas.style.left topLeft.x px; canvas.style.top topLeft.y px; ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0); ctx.clearRect(0, 0, canvas.width, canvas.height); let indices gridIndex.query(bounds); let size Math.max(1, 2 * Math.pow(2, map.getZoom() - 10)); ctx.fillStyle #ff6600; for (let i 0; i indices.length; i) { let p points[indices[i]]; let pixel map.latLngToContainerPoint([p.lat, p.lng]); ctx.fillRect( pixel.x - topLeft.x - size / 2, pixel.y - topLeft.y - size / 2, size, size ); } } map.on(move zoom, function () { requestAnimationFrame(draw); }); draw();这段代码把20w点做网格索引后视觉上可以很顺滑地拖动。要注意的是map.latLngToContainerPoint这个换算在20w数据全量遍历时依然有开销所以我前面才强调要先做网格裁剪只对视野内的点做换算。4. 地图动画怎么做从点扩散到轨迹流动4.1 动画的本质是“每一帧重绘但只画变化的部分”20w数据的地图动画很多人第一反应是“让每一个点都动起来”。这个思路一旦铺开性能立刻崩。真正合理的做法是静态点用离屏Canvas画好放着动画只单独画一小部分需要动的对象。举一个最常见的点扩散动画比如地图上要展示事件热力点的扩散效果。我们不会拿20w个点全部做扩散而是只取其中一个或少数几个重点位置每一帧更新它们的半径和透明度然后重新绘制这些点。let ripples []; function startRipple(lngLat) { ripples.push({ lng: lngLat[0], lat: lngLat[1], startTime: performance.now() }); } let animLayer L.Layer.extend({ onAdd: function () { this._canvas L.DomUtil.create(canvas, ripple-layer); L.DomUtil.addClass(this._canvas, leaflet-zoom-animated); this._ctx this._canvas.getContext(2d); map.getPanes().overlayPane.appendChild(this._canvas); this._animId requestAnimationFrame(this._animate.bind(this)); return this; }, _animate: function () { let now performance.now(); let ctx this._ctx; let bounds map.getBounds(); let topLeft map.latLngToContainerPoint(bounds.getNorthWest()); ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); for (let i ripples.length - 1; i 0; i--) { let r ripples[i]; let t (now - r.startTime) / 2000; if (t 1) { ripples.splice(i, 1); continue; } let radius t * 80; let alpha 1 - t; let pixel map.latLngToContainerPoint([r.lat, r.lng]); ctx.beginPath(); ctx.arc(pixel.x - topLeft.x, pixel.y - topLeft.y, radius, 0, Math.PI * 2); ctx.strokeStyle rgba(255, 100, 0, ${alpha}); ctx.lineWidth 3; ctx.stroke(); } this._animId requestAnimationFrame(this._animate.bind(this)); } });这个动效之所以流畅是因为动画数据量极小每一帧只重绘几个ripple对象。20w静态点不会干扰动画的帧率因为它们在另一张Canvas上。4.2 轨迹流动动画按时间采样而不是让所有点一起动另一种常见地图动画是轨迹流动比如车辆轨迹、物流路线、人员流动。20w数据的场景里你不可能让20w个点位都同时沿着路径动只能分层处理。思路是先渲染静态的完整路径线再用一个移动的“光点”在路径上滑动。实现上轨迹线可以提前生成一个坐标序列光点的位置根据动画进度在序列里做插值function getPointOnPath(path, progress) { if (progress 1) return path[path.length - 1]; let totalLen path.length - 1; let idx progress * totalLen; let i Math.floor(idx); let t idx - i; let p1 path[i]; let p2 path[i 1]; return { lng: p1.lng (p2.lng - p1.lng) * t, lat: p1.lat (p2.lat - p1.lat) * t }; }这里不要用lerp做经纬度直接线性插值以外的复杂算法因为在海量动画的需求里精度和性能要平衡。轨迹点已经是抽稀过的线性插值足够平滑。4.3 动画帧率管理与频率控制浏览器里控制动画帧率的标准工具是requestAnimationFrame简称rAF。它会在每次屏幕刷新前自动调用回调正常是60次每秒。使用rAF而不是setInterval好处很多页面切到后台时rAF会自动暂停、节省CPUrAF和屏幕刷新同步动画不会有撕裂感并且rAF的调用时机精确不会因为事件循环堆积而出现闪烁。但rAF有个小坑如果你在rAF回调里做了大量同步重绘会造成掉帧。所以动画代码里要避免在每一帧做全量坐标换算尽量提前算好屏幕坐标或者只对需要动画的极少数对象做实时换算。此外如果同一个页面里有多个动画图层比如一个点扩散动画加一个轨迹动画建议共用一个rAF循环而不是每个图层各自开一个避免产生大量重复的帧回调。let animating false; function startAnimationLoop() { if (animating) return; animating true; function loop() { updateRipples(); updateTrajectories(); renderAllAnimLayers(); if (hasActiveAnimations()) { requestAnimationFrame(loop); } else { animating false; } } requestAnimationFrame(loop); }这个模式适用于绝大多数地图动画场景并且可以很自然地扩展现有的动画类型。4.4 地图缩放时的动画适配地图动画里还有一个常见问题用户操作地图进行缩放时动画对象的位置要跟着地图变。如果动画里的对象用经纬度存储问题不大每次把经纬度换算成屏幕坐标再画就行。如果动画对象已经存了屏幕坐标那么地图缩放后起到直接偏移动画就会“飘”错位置。我的做法是动画对象的坐标一律用经纬度存储渲染时才换算到屏幕坐标。这样无论用户怎么缩放拖动动画位置始终和地图贴合。缺点是每帧都要做换算但因为动画对象数量少几毫秒内就完成了。如果动画对象数量特别多比如同时有上百个飞线动画那就要引入“缩放期间暂停动画缩放结束后恢复”的策略。监听地图的zoomstart和zoomend事件缩放过程中停止动画更新只保留静态底图缩放结束后重新计算所有动画对象位置并继续动画循环。这个方法能保证缩放地图不卡同时动画也不会错位。5. 性能调优关键指标与实测参数5.1 三个硬指标FPS、首帧耗时、内存占用做20w数据地图性能调优不能靠感觉要有量化指标。我个人在本地会固定测三个数据FPS地图拖动和动画播放时的实时帧率用Chrome DevTools的Performance面板或者Stats.js这类库来测。低于30就需要优化。首帧耗时从数据加载完成到第一帧绘制结束的时间这个影响用户等待感受控制在500ms内算及格。内存占用用DevTools的Memory面板看堆内存变化重点看是否有明显泄漏或并发增长。我实测过的一份20w点数据坐标字段 一个分类字段共约6MB JSON在自研Canvas图层 网格索引的方案下数据解析耗时约260ms网格索引构建耗时约40ms首次全量绘制20w点约180ms拖动地图视野变化后的单帧绘制视野内约3w点约18ms动画图层附加后FPS稳定在55到60之间这些数值可以作为你调优的参考基准。如果你测出来首帧耗时几秒钟那大概率是数据清洗或绘制逻辑出了问题而不是硬件不行。5.2 抽稀策略20w看起来可以更“少”有时候从业务角度看20w点并不是每个都要显示。如果视觉上密集区域已经糊成一团那保留20w个反而没有任何意义。此时可以做视觉抽稀保留关键特征点减少绘制数量这是最简单直接的性能优化方式。抽稀算法推荐使用Douglas-Peucker或者格网抽稀。格网抽稀更好理解把地图范围分成网格每个网格内只保留一个点。比如设定每100米网格保留一个点20w点可能抽到3w点视觉效果基本不变性能提升好几倍。分享一个判断原则如果放大到最大层级后点与点之间仍然有大量重叠说明抽稀还有空间。密集区域的点重叠严重时无论渲染技术多先进用户看到的就是一团色块数据本身的可读性已经丢失了此时降低密度反而是对用户更好的选择。5.3 避免隐藏的性能陷阱有几个隐藏很深的陷阱新手特别容易踩Canvas的width和height属性不要设置为0或过大。width和height超过一定值比如4096很多浏览器会自动禁用Canvas硬件加速性能立刻崩。要按当前视野动态设置不要一开始就设成超大画布。不要把Canvas做成固定全屏大小后一劳永逸。浏览器或移动端的devicePixelRatioDPR如果很高比如2或3Canvas实际像素数是CSS像素的4到9倍20w点绘制也会明显变慢。可以按需限制DPR比如上限取2画面清晰度和性能平衡比较好。地图的zoom事件频繁触发时不要每次都全量重绘。加个防抖或者合并到rAF里能明显降低无效计算。6. 常见问题与排查策略实录6.1 动画卡顿第一反应应该检查什么如果你在20w数据场景里做动画卡成狗排查优先级我觉得是动画循环里是不是每帧都重绘了静态点数据如果是立刻改成离屏Canvas 静态图层分离。动画对象数量是不是太多了单个rAF循环里同时更新的对象超过几千个通常就会开始掉帧。控制动画对象数量在几百以内一般没问题。是不是每帧都在做经纬度到屏幕坐标的批量换算优化的思路是提前在Worker里算好或只在动画对象数量少时实时换算。是不是memory leak长时间运行动画后内存一直涨多半是Canvas或事件监听器没释放。6.2 表格常见问题、原因与解决方向问题现象可能原因解决方向地图拖动卡顿FPS低每次move都全量绘制20w点网格索引裁剪视野 rAF节流 离屏Canvas动画播放时卡顿动画循环里重绘了底图静态点静态点与动画分层动画只画动态对象地图缩放后动画错位动画对象存储了屏幕坐标改用经纬度存储渲染时再做坐标换算初始化白屏很久JSON数据量过大解析阻塞主线程字段精简 数据分片加载 Worker解析内存持续上涨监听器未解绑 / Canvas对象重复创建检查事件绑定释放复用Canvas实例高清屏下模糊或卡顿DPR过高导致Canvas像素暴涨限制DPR上限动态设置Canvas尺寸6.3 排查工具的使用经验Perfomance面板里要把“CPU降速”调成4x或者6x来模拟中低端机环境很多问题在高端机上看不出来降速后就暴露了。另外如果数据是异步加载的去Network面板看加载耗时数据压缩和懒加载策略往往比渲染优化更快见效。还有一个小工具非常实用利用Canvas的ctx.isPointInPath做拾取其实很慢20w数据下根本别指望基于这个做点击交互。如果产品经理要求点击某个点显示详情应该用空间索引命中检测来做或者直接根据点击的屏幕坐标反查附近最近点后者在性能上要友好得多。6.4 Worker把解析和索引构建挪出主线程如果数据量到50w以上建议把JSON的解析、网格索引构建全部放到Web Worker里做只把最终结果通过postMessage传回主线程。这样主线程不会因为大数据处理而卡住地图渲染和交互都能保持流畅。const worker new Worker(data-worker.js); worker.onmessage function (event) { const { indices, bounds } event.data; gridIndex indices; draw(); }; worker.postMessage({ url: /api/points/200k });在Worker里解析好经纬度数组用Float32Array存储通过postMessage的transferable object传回主线程可以避免结构化克隆的内存拷贝进一步省掉一大块开销。这一步对20w来说属于锦上添花但如果项目以后要扩到百万级提前打好这个底子很值。7. 一些额外值得说的经验7.1 热力图的20w是另一回事如果你是想拿20w数据做热力图那方案选择又不一样了。Canvas散点渲染和热力图渲染的差异很大热力图通常用Canvas的createRadialGradient叠加混合模式20w个点的热力图很容易爆。比较稳妥的做法是先用网格聚合比如按当前zoom级别聚合到几千个格子每个格子的值代表权重再对聚合结果做热力渲染。这样性能和热力效果都能兼顾。这里要注意一个常见认知偏差很多人觉得热力图“就是把每一个点画上去然后加模糊”实际上它是基于密度的空间插值。数据点极度密集时直接逐点绘制会产生大量重复计算聚合才是正路。7.2 大数据展示的交互反馈设计性能优化做到一定程度后你会发现用户的卡顿感其实不只是FPS问题还跟“交互反馈是否即时”有关。比如点击一个点如果等两秒才弹窗那就是渲染和交互没有分离的设计问题。我的建议是点击交互时不要立刻查询全量数据而是先从小索引里找到最近的点位ID再根据ID去查详情数据。详情数据如果本来就从接口拿好存在内存里那弹窗就是及时反馈。如果详情数据在服务端点击后要先展示loading同时把这个请求做成异步避免阻塞渲染。7.3 数据分级与LOD的概念最后一个值得说透的点是LODLevel of Detail。地图缩得太小的时候20w个点密密麻麻叠在一起除了视觉噪音什么信息都没有。此时应该根据需要降低展示的数据量比如缩放级别10-12展示聚合后的约2w个代表点缩放级别13-15展示约10w个点缩放级别16以上展示全部20w个点LOD的切换要放在单独的render逻辑里不改变原始数据只改变每帧绘制时取多少个点。这个思路在几乎所有地图大数据的实现里都存在属于基本功但很多人一开始没这个意识结果总在最高级别把所有点一次全画出来自然卡。8. 最后分享一点我个人调地图动画的体会在做这类地图项目的过程中我最大的体感是地图动画的地基不是动画逻辑本身而是你在动画之前打的底子。静态点图层稳了动画图层才能跑得动。静态点图层本身不卡动画才有存在的意义。反之如果静态点都画不利索哪怕动画代码写得再漂亮也是在一个沙地上盖楼。调试的时候我还有个习惯先把数据量从20w降到2w确认动画效果和逻辑没有问题再逐步往上加数据。这样定位问题会特别快是动画逻辑的问题还是渲染性能的问题一拆就清楚。等上升到20w的时候再针对性能做优化不会出现“动画效果没做完就忙着考虑卡不卡”的局面。另外动画循环的代码写法上我建议把动画状态和渲染逻辑拆开。动画状态只是一个对象数组记录位置、起始时间、属性等纯数据渲染逻辑只负责读这些状态然后往Canvas上画。这样做的好处是将来如果数据量再涨你只需要把状态管理放到Worker里渲染还是同一套逻辑代码不用大改。到目前为止上面的方法已经足够支撑绝大多数20w数据量级的地图展示和动画需求。如果你做到了这一层再往上的百万级甚至千万级方向就是WebGL、离屏渲染、后端瓦片化这些更重的武器了回头有机会再单独写写。
返回列表