项目场景:
实现30000+设备的全国布局图,并且设备数量仍在持续增长,如何将海量点渲染至高德地图。通过zoom等级分两个图层(地级市图层和设备图层)请求懒加载设备点,尝试减少http请求,探索开辟webworker对地级市点大小进行计算等性能优化手段。记录一下自己的实现和思考。
1.地级市图层
2.设备图层(单个地级市,如苏州市)
需求描述
1.进入全国布局页面,只渲染地级市点图层,通过deviceSize渲染点的大小。
2.点击级联组件进行搜索城市,定位到所在城市并且请求设备点数据,并且在地图上绘制出来,同时要隐藏所有的地级市点。
3.监听地级市点图层的marker点击事件,定位到该城市并且请求设备点数据,并且在地图上绘制出来,同时要隐藏所有的地级市点。
4.根据鼠标滚轮缩放地图,放大地图,监听zoomchange事件,通过zoom是否大于10来控制地级市点图层和设备点图层的显示隐藏。并且zoom在大于10的情况下才会去请求地图中心点所处的地级市的数据,并且在地图上绘制出来,同时要隐藏所有的地级市点。
优化思路:
1.总体思路,本来想直接使用vue-amap的海量点。但是此方法的缺点1仅支持h5浏览器,2在点数量10万以内有较好的性能表现,超过10万性能无法保证。
最后选择基于 Zoom 层级的懒加载,zoom以10为分界点,小于10则只展示地级市点图层;大于10则展示设备点图层。先加载地级市点,再通过地级市去加载对应的设备点,减少了很多请求。(后端返回的数据如下)
2.通过缓存到sessionStorage减少重复的http请求,请求地级市数据时一次即可拿到数据;
3.请求设备点数据不需要优化,因为是通过地址单个请求,主要减少请求的是向高德api的请求,由于后端返回的是城市名,没有返回经纬度,无法直接在地图上渲染点,需要通过高德的getLocation()获取到城市对应的经纬度,将地级市点渲染出来。流程如下:城市名=>经纬度=>渲染点。
尝试1:分批请求
可以将城市名=>经纬度分批处理,每批处理一定数量的地址,使用Promise.all
,同时控制并发数量,避免触发 API 的调用频率限制。
// 加载城市点(设备总数量)
loadCitiesMarkers() {
// 如果数据加载完成
if (this.citiesData && this.citiesData.length > 0) {
console.log('loadCitiesMarkers with citiesData', this.citiesData);
// 将请求分为若干批次
const batchSize = 6;
const promises: Promise<void>[] = [];
// 分批处理城市数据
for (let i = 0; i < this.citiesData.length; i += batchSize) {
const batch = this.citiesData.slice(i, i + batchSize);
const batchPromises = batch.map(city => {
return new Promise<void>((resolve, reject) => {
this.geocoder.getLocation(city.name, (status: string, result: any) => {
if (status === 'complete' && result.geocodes.length > 0) {
const location = result.geocodes[0].location;
const markerSize = this.calculateMarkerSize(city.deviceSize);
const marker = new AMap.Marker({
position: location,
offset: new AMap.Pixel(-markerSize / 2, -markerSize / 2),
content: `<div style="width:${markerSize}px;height:${markerSize}px;background-color:green;border-radius:50%;"></div>`,
});
this.cityMarkerLayer.addOverlay(marker);
this.citiesMarkers.push(marker);
this.devicesMarkers.push(marker);
// 可以添加鼠标 hover 时显示的信息窗体
const infoWindow = new this.AMap.InfoWindow({
content: `<div>${city.name}</div>
<div>${city.deviceSize}座</div>`,
});
marker.on('mouseover', () => {
infoWindow.open(this.map, marker.getPosition());
});
marker.on('mouseout', () => {
infoWindow.close();
});
// 绑定点击事件,只针对 cityMarkerLayer 的 marker
marker.on('click', () => {
this.zoom = 10;
this.map.setZoomAndCenter(this.zoom, marker.getPosition());
infoWindow.close();
});
resolve(); // 请求成功,完成当前 promise
} else {
console.error(`Geocoding failed for city: ${city.name}`);
reject(`Geocoding failed for city: ${city.name}`);
}
});
});
});
// 将该批次的所有 promises 添加到主 promises 数组中
promises.push(...batchPromises);
}
// 等待所有批次的请求完成
Promise.all(promises)
.then(() => {
console.log('All city markers loaded successfully');
})
.catch((error) => {
console.error('Error loading city markers:', error);
ElMessage.error('加载城市标记时发生错误,请稍后重试');
});
} else {
console.log('城市设备数量未加载完毕!请稍等一会儿!');
ElMessage.warning('城市设备数量未加载完毕!请稍等一会儿!');
}
}
结果:失败
每个城市依然单独发起请求: 尽管代码使用了 Promise.all
进行批处理,但 Promise.all
只是在并发处理多个请求,并没有减少实际的请求数量。代码中的 this.geocoder.getLocation(city.name)
方法还是为每个城市名单独发起一个请求。
高德地图 API 本身的限制: 高德地图的 getLocation
方法本质上是一个单一的地理编码请求,也就是说它一次只能处理一个城市名,并返回相应的经纬度。所以即使批量处理,这些请求仍然是一个一个发出的。
提前缓存已知城市的经纬度: 如果已经请求到了一些城市和它们的经纬度,可以先将这些数据缓存下来,不再重复请求。例如,使用 sessionStorage
或 localStorage
存储这些数据,在页面加载时首先检查缓存中是否存在相应的城市经纬度信息。但是全国有3000+地级市,这个信息缓存的大小也很大,这里我就没有选择缓存。
- 最优解:后端处理请求,返给前端经纬度,这样不需要调用高德的API。
感悟:这里意识到不同岗位都有自己的职能上限,如果后端返回的数据不合适,在前端如何尝试优化可能效果都不如后端把处理好的合适的数据返回来,来得简单和高效。
4. 地图两个图层,通过监听zoomchange来控制显示隐藏,同时有两个marker数组来维护已经请求过的数据,类似缓存功能,只有没请求过的地级市才请求。监听zoomchange时间做防抖,从地图获取中心位置所在地级市的经纬度也需要做防抖。
6. js计算地级市点大小,探索是否需要开辟webworker来做。由于有很多地级市设备deviceSize很大,而且城市很接近,尽量让相近城市群的点大小不要互相遮挡,设计一个算法需要计算每个点的大小。因为点数量还在增加中,不确定海量的数值计算是否影响js的执行时间。尝试查看js执行时间,并发现好像计算耗时不是很多,所以最后没有使用webworker(关于js某个函数执行时间和渲染时间不太了解,这一块还需要学习,这里记录一下)。
总结:
记录了一下实习中实现30,000+设备的全国布局图的过程,并探讨了如何在高德地图上渲染海量点的性能优化方案。使用基于Zoom层级的懒加载策略,并通过sessionStorage减少重复请求,同时尝试分批请求和WebWorker优化。欢迎大家讨论并指出改进点。