当地图中存在大量区域、标签等添加物的时候,会造成初次渲染等待很长时间、操作严重卡顿等问题。优化前常规写法就是后端给多少数据我创建多少区域与标签到地图上,在开发过程中发现当有3000条数据的时候地图会卡顿10秒左右时间才能渲染完成,并且操作体验也很差。
需要针对这种情况去优化前端代码而不是去改动业务(对,说的就是不用点聚合!),我总结了以下几点经验(业务需求,就不贴图了):
- 调整初始化视口大小,优化首屏加载速度
- 切割数据数组,分组轮询
- 根据地图事件轮询更新信息
- 根据当前地图缩放大小判断是否显示区域、标签
PS:本文章用的是高德地图API
首屏加载优化
调整缩放值是为了让初始化地图的可视区域不会过大:
js
map.setCenter(new AMap.LngLat(centerLo, centerLa))
map.setZoom(17)
当然这样做主要是为了下一步,根据当前地图可视区域,渲染可视的标签与区域等信息:
js
const bounds = map.getBounds() // 获取地图可视区域的边界
const list = [...] // 这是个很长很长的数组
const markers = this.markers // 已经生成的标签
const polygons = this.polygons // 已经生成的区域
for (let index = 0; index < list.length; index++) {
const item = list[index]
const poi = new AMap.LngLat(item.lon, item.lat)
const contains = bounds.contains(poi) // 判断当前点是否在地图可视区域内
if (contains && !markers[index]) {
// 没有创建的情况
const marker = new AMap.Marker({...})
marker.show()
markers[index] = marker
const polygon = new AMap.Polygon({...})
polygon.show()
polygons[index] = polygon
} else if (contains) {
// 创建过且在可视区域的情况
markers[index].show()
polygons[index].show()
} else if (markers[index]) {
// 创建过且不在可视区域的情况
markers[index].hide()
polygons[index].hide()
}
}
这样处理的原因是,首屏加载过慢的核心原因在于一次性创建了太多地图上的实例并且把它们都渲染到地图上,通过上面的方式可以让首屏只加载很少的地图中实例而不影响使用。
数组切割
其实当数据数组过长的时候,本身轮询一次就已经很耗时了,所以把数组切割开来处理也会好很多,我们接着上面的代码优化:
js
function chunkArray (chunkedArr, chunkSize) {
const chunkedArr = []
let i = 0
while (i < arr.length) {
chunkedArr.push(arr.slice(i, i + chunkSize))
i += chunkSize
}
return chunkedArr
}
this.listChunk = chunkArray(list, 100)
切开数组后我们分组轮询:
js
for (let i = 0; i < this.machineChunk.length; i++) {
// 用定时器强行把数组加载分开 防止同一帧内轮询次数过多
setTimeout(() => {
chunkLoad(this.listChunk, i)
}, i * 200)
}
function chunkLoad (list, chunkIndex) {
...
// 使用requestAnimationFrame可以在使加载更加平滑
requestAnimationFrame(()=>{
for (let index = 0; index < list[chunkIndex].length; index++) {
const realIndex = chunkIndex * 100 + index // 先算出真实的数组index
...
if (contains && !markers[realIndex]) {
...
markers[realIndex] = marker
polygons[realIndex] = polygon
} else if (contains) {
markers[realIndex].show()
polygons[realIndex].show()
} else if (markers[realIndex]) {
markers[realIndex].hide()
polygons[realIndex].hide()
}
}
})
}
事件更新
到这里我们主要的优化逻辑已经实现,接下来就是根据地图可视区域变化,重新走一遍上面的逻辑:
js
const event = () => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
for (let i = 0; i < this.machineChunk.length; i++) {
setTimeout(() => {
chunkLoad(this.listChunk, i)
}, i * 200)
}
}, 100)
}
this.map.on('moveend', () => {
event()
})
缩放更新
虽然我们已经让页面动态渲染了,但是我们只要一直缩小总能看到地图所有信息,这里有个问题就是当缩小到一定程度,地图上上千个点堆叠在一起,本身已经没有了意义,所以还需要根据缩放信息去判断页面上的信息是否应该展示或者说换一种方式展示。
首先是区域信息,如果我们地图上画的区域都是百米范围的,那当地图缩放到zoom
值为15的时候,在地图上基本上就是一个点了,所以这时候有个区域中心点的标签就够了:
js
const zoom = map.getZoom()
...
requestAnimationFrame(()=>{
for(...) {
...
// zoom值小于15直接隐藏
if (zoom >= 15) polygons[realIndex].show()
else polygons[realIndex].hide()
}
})
同理我们地图再缩小,那么上千个标签也就堆叠到一起了,信息已经无法辨认,这时候我们在适当的zoom
值把所有标签替换为圆形标签,简化显示方式:
js
const zoom = map.getZoom()
const circleMarker = this.circleMarker
...
requestAnimationFrame(()=>{
for(...) {
...
const cMarker = new AMap.CircleMarker({...})
circleMarker[realIndex] = cMarker
// zoom值小于13直接换成圆形标签
if (zoom <= 13) {
markers[realIndex].hide()
circleMarker[realIndex].show()
} else {
markers[realIndex].show()
circleMarker[realIndex].hide()
}
}
})
其他
如果数据量不是很大,那其实不用上面第一步的调整缩放值。除了渲染逻辑上的优化,代码角度来看for
循环比forEach
性能要好很多,所以把所有轮询改为for
。说了这么多,其实优化的目的就是增加操作流畅性的同时不影响业务,所以优化方式千千万,具体业务具体方案,欢迎交流。