????????最近剛做了個和地圖相關(guān)的需求,涉及到「海量點標記 + 海量標注」。當數(shù)據(jù)量達到三千以上的時候,「海量標注」會明顯拖慢頁面的加載/響應(yīng)速度,非常影響用戶體驗,因此我對其進行了優(yōu)化。感覺還挺有挑戰(zhàn)性的,在這里總結(jié)一下,關(guān)鍵性代碼(Vue3)已開源至 GitHub:https://github.com/yingjieweb/amap-optimization-demo,如果能給遇到同樣問題的你一點幫助的話,麻煩幫忙點個 star~???。
? ? ? ? ?? 如果你所做的需求僅涉及到「海量點標記」,那你不需要看這篇博客,因為高德地圖的「海量點標記」已經(jīng)優(yōu)化的很好了,它可以為數(shù)量在萬級以上的點標記提供良好的解決方案,這里有高德地圖海量點標記的「官方文檔」和「實例」可以參考。
?????????? 本篇博客針對數(shù)據(jù)量較大時,「海量標注」會拖慢頁面的加載/響應(yīng)速度的問題做了分析和優(yōu)化實踐,其中心思想分為以下幾點:
- 通過將?海量標注?延遲加載(懶加載)的方式,加快頁面首屏渲染速度
- 檢測?海量標注?中的數(shù)據(jù)項,判斷其坐標是否在瀏覽器視口區(qū)域,進行分片渲染
- 監(jiān)聽地圖縮放及移動事件,先刪除原標注圖層,再根據(jù)第二步渲染新圖層
- 對?海量標注?中的公共部分進行提取,通過?海量點標記 的方式渲染,減少 DOM 節(jié)點數(shù)
- 針對點擊后高亮被選中的標注,單獨添加圖層進行疊加,減少第三步帶來的延遲
1、適用場景說明
? ? ? ? 這里大致描述一下我所做的需求,大家可以類比一下,進而判斷是否是適用于你 ???
? ? ? ? 我的需求是這樣的:用戶進入頁面時先加載地圖,并在地圖上展示一些點標記。當?shù)貓D被放大到一定倍數(shù)的時候,隱藏點標記圖層,展示標注圖層。標注圖層里的每個標注有一個小的圖點標識圖和氣泡組成,氣泡內(nèi)含有一些數(shù)據(jù)信息。當用戶點擊某個氣泡時,需要高亮當前氣泡,并將其展示在最上層。
? ? ? ? 效果圖大致如下:
?2、地圖及海量點標記
? ? ? ? 下面就從地圖搭建開始,一步步實現(xiàn)上面的功能,然后對頁面卡頓問題進行優(yōu)化。因為幾乎都是調(diào)用高德地圖提供的 API,所以我在這里只粘貼部分代碼,算是提供個思路吧 ???
? ? ? ? 關(guān)于初始化地圖和設(shè)置「海量點標記」的主要是思路如下:
geocoder = new AMap.Geocoder({
city: cityId // 支持傳入城市名、adcode 和 citycode
})
geocoder.getLocation(cityName.value, function(status, result) {
if (status === 'complete' && result.info === 'OK') {
map = new AMap.Map('map', {
resizeEnable: true, //是否監(jiān)控地圖容器尺寸變化
zoom: 10, //初始化地圖層級
center: [result.geocodes[0].location.lng, result.geocodes[0].location.lat], //初始化地圖中心點
}); // 展示地圖 map
setMassMarks() // 設(shè)置海量點標記 marker
}
})
? ? ? ? 設(shè)置「海量點標記」的方法如下:
setMassMarks() {
const lowPriceIconStyle = { url: priceIconMap.low, size: new AMap.Size(30, 30), anchor: 'center' }
const normalIconStyle = { url: priceIconMap.normal, size: new AMap.Size(30, 30), anchor: 'center' }
const highPriceIconStyle = { url: priceIconMap.high, size: new AMap.Size(30, 30), anchor: 'center' }
const styleObjectArr = [lowPriceIconStyle, normalIconStyle, highPriceIconStyle]
const markerList = houseList.map(item => ({
lnglat: item.location.split(','),
name: item.name,
id: item.id,
style: item.level - 1
}))
massMarks = new AMap.MassMarks(markerList, {
zIndex: 500, // 海量點圖層疊加的順序
zooms: [3, 14], // 在指定地圖縮放級別范圍內(nèi)展示海量點圖層
style: styleObjectArr // 設(shè)置樣式數(shù)組
});
massMarks.setMap(map);
}
? ? ? ?上面??? 兩段代碼主要是先初始化地圖,然后用 cityName 通過 AMap.Geocoder (高德地圖的編碼轉(zhuǎn)換器) 獲取城市的具體坐標,從而設(shè)置地圖的中心點坐標。然后通過高德的 AMap.MassMarks 方法設(shè)置「海量點標記」展示在 3-14 zoom 層級,再通過?style 參數(shù)做點標記的樣式區(qū)分。
3、海量標注
? ? ? ? 頁面一進入的「海量點標記」圖層已經(jīng)實現(xiàn)了,下面是設(shè)置「海量標注」:
setLabelsLayer() {
labels = []
houseList.map(item => {
const normalMarker = new AMap.Marker({
zooms: [14, 20],
offset: new AMap.Pixel(0, -15),
extData: item
});
normalMarker.setContent(
`<div class="amap-info-window">
<div class="amap-info-title">${item.name}</div>
<div class="amap-info-price">${item.location}</div>
<img class="amap-info-dot" src="https://t1...jpg">
</div>`);
normalMarker.setPosition(item.location.split(','))
normalMarker.on('click', function(e){
setNormalMarkerSelected(e.target.getExtData().id)
});
labels.push(normalMarker)
})
map.add(labels)
}
? ? ? ? 關(guān)于「海量標注」,高德地圖的官方解釋是:當需要在地圖添加千級以上的點標記時,LabelMarker 是代替 Marker 的更好選擇。不同于 MassMarks ,LabelMarker 不僅可以繪制圖標,還可以為圖標添加文字信息,且萬級以上數(shù)據(jù)也具有較好性能,配置也更加靈活。
????????但是其實性能并沒有那么好,當數(shù)據(jù)量達到三千級以上的時候,LabelMarker 的畫面流暢程度大大縮減了,基本上是用戶所不能接受的地步,因此我們需要對其進行優(yōu)化。
? ? ? ? ps:不知道是不是我打開方式有問題,有沒有人使用過「海量標注」,并像官方所說的那樣:萬級以上數(shù)據(jù)也具有較好性能?我標記了三千多個標注的時候頁面的響應(yīng)速度就會超級卡,如果大家有更好的思路的話請指教 ??
4、卡頓優(yōu)化實踐
? ? ? ? 先說一下我們上面的思路吧:進入頁面加載地圖,并設(shè)置「海量點標記」和 「海量標注」。
? ? ? ??? 這算是一個最簡易版本,雖然我們已經(jīng)采用了高德地圖所提倡的「海量點標記」和 「海量標注」,但是無論是頁面的首屏加載速度還是用戶交互的響應(yīng)速度都很慢,為了增強用戶體驗,我們需要在上面思路的基礎(chǔ)上進行優(yōu)化!
? ? ? ?1、針對首屏加載速度的優(yōu)化:其實用戶一進入到頁面看到的是「海量點標記」的圖層,「海量標注」圖層是隱藏在后面的,當用戶放大地圖的時候才會展示出來。因此,我們可以先把「海量標注」的加載放到后面,這樣可以加快頁面首屏渲染速度。
? ? ? ?既然要把「海量標注」放在后面加載,那我們就需要監(jiān)聽地圖的縮放事件,當?shù)貓D放大到一定級別時就去加載「海量標注」圖層:
setMapListener() {
map.on('zoomend', () => { // 監(jiān)聽地圖縮放結(jié)束后的等級
zoom = map.getZoom()
if (zoom >= 14) {
setLabelsLayer() // 設(shè)置海量標注 具體代碼在上面
}
})
}
? ? ? ? 把「海量標注」圖層的加載時機放在后面雖然可以加快首屏的渲染速度,但是當數(shù)據(jù)量較大的時候,一次性加載超多「海量標注」的 DOM 元素會讓瀏覽器的渲染引擎吃不消的。既然這樣我們可不可以不要一次性加載那么多?先把視口區(qū)域的標注加載出來?
? ? ? ?2、針對海量標注的分片加載優(yōu)化:檢測「海量標注」中的數(shù)據(jù)項,判斷其坐標是否在瀏覽器視口區(qū)域,從而進行分片渲染:
executeConditionRender() {
let screenCoordinateRange = map.getBounds()
let northEast = [screenCoordinateRange.northEast.lng, screenCoordinateRange.northEast.lat]
let southEast = [screenCoordinateRange.southWest.lng, screenCoordinateRange.northEast.lat]
let southWest = [screenCoordinateRange.southWest.lng, screenCoordinateRange.southWest.lat]
let northWest = [screenCoordinateRange.northEast.lng, screenCoordinateRange.southWest.lat]
screenHouseList = houseList.filter(item => {
return AMap.GeometryUtil.isPointInRing(item.location.split(','), [northEast, southEast, southWest, northWest])
})
setLabelsLayer() // 設(shè)置海量標注 具體代碼在上面,用 screenHouseList 替換 houseList
}
? ? ? ? 上述代碼中,我們可以通過高德地圖提供的?getBounds 方法,拿到當前視口中地圖的東北角和西南角的坐標,進而可以算出視口的四個點坐標,然后通過?AMap.GeometryUtil.isPointInRing 方法,判斷海量標注圖層中的數(shù)據(jù)是否在視口范圍內(nèi),進行分片加載。
? ? ? ? 除此之外我們還需要監(jiān)聽用戶的滑動事件,當用戶滑動地圖之后,重新執(zhí)行如上邏輯:
const setMapListener = () => {
map.on('zoomend', () => { // 監(jiān)聽地圖縮放結(jié)束后的等級
zoom = map.getZoom()
if (zoom >= 14) {
executeConditionRender()
}
})
map.on('moveend', () => { // 監(jiān)聽地圖中心點的位置變化
if (zoom >= 14) {
executeConditionRender()
}
})
}
? ? ? ?我們上述的編碼思想是:當用戶放大或移動視口時,先判斷當前 zoom 是否在海量標注展示的級別,如果是的話就加載當前視口內(nèi)的標注。但是這樣會產(chǎn)生一個問題,標注圖層的數(shù)量會隨著用戶的操作次數(shù)的增加而堆積,這樣也是會影響頁面流暢程度的,因此我們需要對其進行優(yōu)化。
????????3、針對海量標注圖層堆積問題的優(yōu)化:在下一次加載視口區(qū)域內(nèi)的標注圖層之前,我們需要先把之前的圖層刪除掉,這樣就可以避免圖層堆積的問題:
executeConditionRender() {
let screenCoordinateRange = map.getBounds()
let northEast = [screenCoordinateRange.northEast.lng, screenCoordinateRange.northEast.lat]
let southEast = [screenCoordinateRange.southWest.lng, screenCoordinateRange.northEast.lat]
let southWest = [screenCoordinateRange.southWest.lng, screenCoordinateRange.southWest.lat]
let northWest = [screenCoordinateRange.northEast.lng, screenCoordinateRange.southWest.lat]
screenHouseList = houseList.filter(item => {
return AMap.GeometryUtil.isPointInRing(item.location.split(','), [northEast, southEast, southWest, northWest])
})
map.remove(labels) // 刪除原來氣泡層的海量標注
setLabelsLayer() // 設(shè)置海量標注 具體代碼在上面,用 screenHouseList 替換 houseList
}
? ? ? ? 到這里,我們主要的優(yōu)化工作已經(jīng)做的差不多了,頁面已經(jīng)是比較流暢的了。上面說的「中心思想」中的第四步(提取公共標識圖)和第五步(高亮選中的標注)相對來說比較簡單了,這里就簡單說一下吧:
? ? ? ?4、針對屏內(nèi)標注 DOM 元素過多的優(yōu)化:第四步(提取公共標識圖)是因為每個標注都帶一個小的圓點圖片,每個小圓點圖片都是一個 DOM 元素,當海量標注比較密集的時候,一屏內(nèi)也能有成百上千個標注,也就會有成百上千個小圓點圖片的 DOM 元素,這會給頁面繪制速度增加負擔(dān)的,這里也是可以優(yōu)化一下的。
????????對?海量標注?中的公共部分進行提取,通過?海量點標記 的方式渲染,減少 DOM 元素,相當于是借用了高德地圖的海量點標記來幫我們做的一次優(yōu)化!具體代碼比較簡單,這里就不貼了,只是新加了一個圓點標記圖層。
????????5、針對高亮標注數(shù)據(jù)處理延遲的優(yōu)化:我們之前的第三步優(yōu)化是針對海量標注圖層堆積的問題處理,它其實做了兩件事:先刪除原來的圖層,然后再遍歷一下原視口中的標注點,將被點擊的標注進行高亮樣式處理,沒被點擊的標注進行重新繪制。
? ? ? ? 上述這種處理方法其實是有優(yōu)化空間的,我的想法是通過單獨添加高亮標注圖層來節(jié)省原圖層的重新計算和渲染,這樣就相當于節(jié)省了第三步中刪除圖層和重新繪制的成本,可能不是很形象,我們畫一下大致流程就很好明白了:
// 優(yōu)化前:
用戶點擊標注 ???刪除舊圖層 ?? 重新計算(點擊的標注設(shè)置高亮,普通標注重新繪制)??? 頁面添加新的標注圖層
// 優(yōu)化后:
用戶點擊標注 ?? 刪除舊的高亮圖層??? 重新設(shè)置高亮圖層 ???頁面添加新的標注圖層
? ? ? ? 這樣看起來直觀很多,其實就是相當于節(jié)省了一部分「重新計算」的成本。
5、優(yōu)化效果對比
? ? ? ? 在數(shù)據(jù)量較大的情況下(我的數(shù)據(jù)量在3500+),通過上述的五個步驟的優(yōu)化之后,頁面的各方面性能都有了明顯的提升,下面我們做一下優(yōu)化前后的性能對比,一起感受一下優(yōu)化之后的絲滑吧!??
? ? ? ? 1、首屏加載時間對比:這也是對上述「1、針對首屏加載速度的優(yōu)化」的優(yōu)化驗證,通過將「海量標注」延遲加載的方式,大大加快了頁面首屏的加載速度,對比圖如下:
? ? ? ? ?沒有將「海量標注」做推遲加載處理時,頁面首屏加載地圖展示出來的時間在 12s 左右,而且還是不完全展示(只展示了海量點標記,地圖沒真正展示出來)。優(yōu)化之后,地圖和「海量點標記」完全加載出來只用了 3s,這個時間雖然不能說很快,但是 3s 的時間普通用戶是完全可以接受的。優(yōu)化之后,首屏的加載時間可以說提升了 4 倍!amazing!??
? ? ? ? 2、幀率統(tǒng)計對比:這也是對上述「2、針對海量標注的分片加載優(yōu)化」的優(yōu)化驗證,通過只展示屏內(nèi)的標注,可以大大縮減海量標注的數(shù)量,進而在用戶和頁面發(fā)生交互行為時,頁面的響應(yīng)速度會更快、幀率更高,對比圖如下:
? ? ? ? 在沒做「海量標注」分片加載時,?一次性將大量的標注信息渲染出來,不僅需要很長的加載時間,而且頁面上的標注節(jié)點太多,會導(dǎo)致用戶和頁面發(fā)生交互行為之后,頁面的響應(yīng)速度很慢,平均幀率只有不到 10 fps。優(yōu)化之后,平均幀率可以接近 30 fps。要知道,人眼在 24幀/s 的情況下就會產(chǎn)生暫留效果,30 fps 可以說是很流暢了!wonderful!??
? ? ? ? 3、DOM 節(jié)點數(shù)對比:這是對上述「2、針對海量標注的分片加載優(yōu)化 + 3、針對海量標注圖層堆積問題的優(yōu)化 + 4、針對屏內(nèi)標注 DOM 元素過多的優(yōu)化」的優(yōu)化驗證,因為上述優(yōu)化點都有對減少 DOM 節(jié)點直接或間接產(chǎn)生貢獻,對比圖如下:?
? ? ? ???沒有做上述「2 / 3 / 4」步驟點優(yōu)化時,頁面的 DOM 節(jié)點數(shù)量在 40000+,由于頁面的 DOM 節(jié)點較多,所以當用戶和頁面發(fā)生交互行為時,CPU的占用率在 90% 以上,JS 的內(nèi)存體積也在 250M+,進而導(dǎo)致頁面卡頓明顯。優(yōu)化之后,可以將頁面上的 DOM 節(jié)點數(shù)控制在 6000 以下,此時當用戶和頁面交互時,CPU 的占用率在 50% 左右,JS 的內(nèi)存體積在 100M 以下,因此頁面交互會流暢很多!??
? ? ? ? 4、整體性能統(tǒng)計對比:通過上述五個步驟的優(yōu)化,頁面的各方面性能都有了一個明顯的提升,我們看一下整體的 Summary 看板的統(tǒng)計指標,對比圖如下:
優(yōu)化前后 Summary 指標對比 | ||
指標 | 優(yōu)化前 | 優(yōu)化后 |
Loading | 255ms | 8ms |
Scripting | 5122ms | 2118ms |
Rendering | 3843ms | 103ms |
Painting | 33ms | 35ms |
Total | 12956ms | 4306ms |
? ? ? ?通過 Summary 的各項指標以及真實的使用體驗可以看出,上述所做的五個優(yōu)化步驟效果還是很明顯的,無論是頁面首屏加載速度,還是交互響應(yīng)速度,以及幀率、DOM 節(jié)點數(shù)...,各方面性能都有一個明顯的提升。What a wonderful world!??
6、寫在最后
? ? ? 這是第一次做和地圖強相關(guān)的需求,一開始以為沒什么難度,就是照著高德?API 抄一抄就行了,沒想到數(shù)據(jù)量較大時還是有很多可以鉆研和學(xué)習(xí)的地方,真是?surprise ???
????????做這個需求幾天里,心里一直被這個事情占據(jù)著,覺都沒睡好,做夢都是考試找不到考場 ??。不過最后還是搞定了,心里不由的開心和興奮,感覺自己又進步了!
? ? ? ? 一開始遇到海量標注卡頓的問題,我先請教公司前輩,但是沒人用過海量標注,進而也就沒遇到過卡頓的問題。但是同事們也給了些思路,我也靜下心來慢慢做了分析,也從網(wǎng)上找到了點優(yōu)化思路,感覺各種優(yōu)化的方法還是有跡可循的。
? ? ? ? 畢業(yè)工作快一年了,這個需求也算是我在無人指導(dǎo)的情況下獨自完成的,完成之后非常有成就感的,我也把它視為一個我成長的里程碑。Keep running!
? ? ? ? 說實話,工作之后自己的時間卻是少了,主動學(xué)習(xí)寫博客的次數(shù)也越來越少,想想畢業(yè)時給自己定的目標「踏踏實實學(xué)點前端,以后做一名專業(yè)的前端開發(fā)工程師」,著實感覺有點慚愧,反思一下自己,還是得持續(xù)學(xué)習(xí)??!文章來源:http://www.zghlxwxcb.cn/news/detail-444734.html
????????ps:寫這篇博客時,感覺好像回到了寫畢業(yè)論文的時候 ??文章來源地址http://www.zghlxwxcb.cn/news/detail-444734.html
到了這里,關(guān)于高德地圖「海量點標記 + 海量標注」卡頓問題 解決方案的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!