博客 / 詳情

返回

關於地圖渲染加20w數據展示和地圖動畫怎麼做

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

前端性能優化實戰:ECharts地圖渲染12萬+數據動態動畫方案

本文記錄了在實際項目中,使用ECharts地圖組件渲染12萬+設備安裝數據的性能優化實戰經驗,包含完整的技術方案和代碼實現。

項目背景

公司需要將全年設備安裝量通過旗幟的形式展示在全國地圖上,實現數據可視化大屏。主要技術挑戰:

  • 數據量大:全年設備安裝數據約20萬條
  • 實時更新:通過WebSocket實時接收數據
  • 動畫效果:需要展示數據逐條添加的動態效果
  • 性能要求:需要保持60fps的流暢動畫

一、初始實現與性能瓶頸

1.1 基礎地圖配置

首先使用ECharts搭建基礎地圖框架:

initChart() {
    this.chart = echarts.init(this.$refs.chart);
    let option = {
        geo: {
            map: 'china',
            roam: true,
            zoom: 1.1,
            scaleLimit: { min: 1, max: 10 },
            itemStyle: {
                areaColor: 'rgba(91,97,141,.3)',
                borderColor: 'rgba(0,0,0,.2)'
            }
        },
        series: [
            // 基礎地圖層
            {
                type: 'map',
                map: 'china',
                itemStyle: {
                    areaColor: 'rgba(0,0,0,0)',
                    borderColor: 'rgba(255,255,255,1)',
                }
            },
            // 設備點圖層
            {
                id: 'scatter',
                type: 'scatter',
                coordinateSystem: 'geo',
                data: [],
                symbol: 'image://flag.png',  // 旗幟圖標
                symbolSize: [16, 22],
                animation: false  // 關閉內置動畫
            }
        ]
    };
    this.chart.setOption(option);
}

1.2 動畫設計

設計旗幟生長動畫效果,通過隨機數實現多樣化的動畫展示:

// 旗幟動畫效果設計
const flagAnimations = [
    'scaleUp',      // 從小變大
    'fadeIn',       // 淡入
    'bounceIn',     // 彈跳進入
    'rotateIn'      // 旋轉進入
];

function getRandomAnimation() {
    return flagAnimations[Math.floor(Math.random() * flagAnimations.length)];
}

1.3 遇到的性能瓶頸

當數據量達到3-5萬條時,開始出現明顯卡頓:

  • 動畫幀率下降到30fps以下
  • 內存佔用持續增長
  • 縮放平移操作卡頓
  • WebSocket數據堆積

二、分層策略優化方案

經過調研,我們採用了分層策略來優化性能,根據不同的縮放級別採用不同的渲染策略。

2.1 分層策略設計

const zoomConfigs = {
    low: {  // 低縮放級別:全國視圖
        zoom: 2,
        sampleRate: 0.1,    // 10%抽樣顯示
        precision: 2,       // 經緯度精度:小數點後2位
        symbolSize: [8, 11] // 縮小圖標
    },
    mid: {  // 中縮放級別:省級視圖
        zoom: 5,
        sampleRate: 0.5,    // 50%抽樣顯示
        precision: 3,       // 經緯度精度:小數點後3位
        symbolSize: [12, 16]
    },
    high: { // 高縮放級別:市級視圖
        zoom: 10,
        sampleRate: 1,      // 100%顯示
        precision: 4,       // 經緯度精度:小數點後4位
        symbolSize: [16, 22]
    }
};

2.2 動態動畫調度系統

class AnimationScheduler {
    constructor() {
        this.pendingList = [];      // 待處理數據隊列
        this.allDeviceList = [];    // 所有數據存儲
        this.displayList = [];      // 當前顯示數據
        this.deviceSet = new Set(); // 數據去重
        this.displaySet = new Set(); // 顯示去重
        this.animationTimer = null;
        
        // 幀率控制
        this.frameInterval = 50;    // 20fps
        this.batchSize = 100;       // 每批處理數量
    }
    
    // 啓動動畫調度
    startAnimation() {
        if (this.animationTimer) return;
        
        let lastTime = 0;
        const animate = (currentTime) => {
            if (this.pendingList.length === 0) {
                this.stopAnimation();
                return;
            }
            
            // 幀率控制
            if (currentTime - lastTime >= this.frameInterval) {
                lastTime = currentTime;
                this.processBatch();
            }
            
            this.animationTimer = requestAnimationFrame(animate);
        };
        
        this.animationTimer = requestAnimationFrame(animate);
    }
    
    // 處理一批數據
    processBatch() {
        const batch = this.pendingList.splice(0, this.batchSize);
        const config = this.getCurrentZoomConfig();
        let hasNewData = false;
        
        batch.forEach(item => {
            // 全局去重
            const globalKey = `${item.lng},${item.lat}`;
            if (this.deviceSet.has(globalKey)) return;
            this.deviceSet.add(globalKey);
            
            const point = {
                value: [item.lng, item.lat],
                createTime: item.createTime
            };
            this.allDeviceList.push(point);
            
            // 根據當前縮放級別抽樣
            if (this.shouldDisplay(point, config)) {
                const displayKey = this.getDisplayKey(point, config);
                if (!this.displaySet.has(displayKey)) {
                    this.displaySet.add(displayKey);
                    this.displayList.push(point);
                    hasNewData = true;
                }
            }
        });
        
        // 批量更新圖表
        if (hasNewData) {
            this.updateChart();
        }
    }
}

2.3 智能顯示判斷

// 根據縮放級別判斷是否顯示
shouldDisplay(point, config) {
    // 完全顯示模式
    if (config.sampleRate >= 1) return true;
    
    // 抽樣顯示模式
    const displayChance = Math.random();
    return displayChance < config.sampleRate;
}

// 生成顯示鍵(根據精度去重)
getDisplayKey(point, config) {
    const lng = point.value[0].toFixed(config.precision);
    const lat = point.value[1].toFixed(config.precision);
    return `${lng},${lat}`;
}

2.4 縮放級別變化處理

// 監聽縮放變化
setupZoomListener() {
    this.chart.on('georoam', () => {
        const option = this.chart.getOption();
        if (option.geo && option.geo[0]) {
            const newZoom = option.geo[0].zoom;
            if (Math.abs(newZoom - this.currentZoom) > 0.1) {
                this.currentZoom = newZoom;
                this.handleZoomChange();
            }
        }
    });
}

// 處理縮放變化
handleZoomChange() {
    const config = this.getCurrentZoomConfig();
    
    // 只有層級變化時才重建顯示數據
    if (config.level !== this.currentZoomLevel) {
        this.currentZoomLevel = config.level;
        this.rebuildDisplayList();
    }
}

// 重建顯示數據
rebuildDisplayList() {
    const config = this.getCurrentZoomConfig();
    this.displayList = [];
    this.displaySet = new Set();
    
    if (config.sampleRate >= 1) {
        // 全量顯示模式
        this.displayAllData(config);
    } else {
        // 抽樣顯示模式
        this.displaySampledData(config);
    }
    
    this.updateChart();
}

// 全量顯示(高精度去重)
displayAllData(config) {
    for (const item of this.allDeviceList) {
        const key = this.getDisplayKey(item, config);
        if (!this.displaySet.has(key)) {
            this.displaySet.add(key);
            this.displayList.push(item);
        }
    }
}

// 抽樣顯示
displaySampledData(config) {
    const step = Math.max(1, Math.floor(1 / config.sampleRate));
    for (let i = 0; i < this.allDeviceList.length; i += step) {
        const item = this.allDeviceList[i];
        const key = this.getDisplayKey(item, config);
        if (!this.displaySet.has(key)) {
            this.displaySet.add(key);
            this.displayList.push(item);
        }
    }
}

三、其他優化技巧

3.1 內存管理優化

// 定期清理過期數據
setupMemoryManagement() {
    setInterval(() => {
        // 限制總數據量
        const maxTotal = 150000;
        if (this.allDeviceList.length > maxTotal) {
            const removeCount = this.allDeviceList.length - 120000;
            this.allDeviceList.splice(0, removeCount);
            
            // 清理相關緩存
            this.cleanCache();
            
            // 重建顯示
            this.rebuildDisplayList();
        }
    }, 30000); // 每30秒檢查一次
}

// WebSocket數據流控
setupWebSocketFlowControl() {
    let buffer = [];
    let processing = false;
    
    this.ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        buffer.push(...data);
        
        // 流量控制:如果緩衝過多,暫停接收
        if (buffer.length > 5000 && !processing) {
            this.ws.pause();
        }
        
        if (!processing) {
            this.processWebSocketBuffer();
        }
    };
}

3.2 性能監控

// 添加性能監控
setupPerformanceMonitor() {
    let frames = 0;
    let lastTime = performance.now();
    
    const monitor = () => {
        frames++;
        const currentTime = performance.now();
        
        if (currentTime - lastTime >= 1000) {
            const fps = Math.round(frames * 1000 / (currentTime - lastTime));
            console.log(`當前FPS: ${fps}`);
            
            // 動態調整策略
            this.adjustStrategyByFPS(fps);
            
            frames = 0;
            lastTime = currentTime;
        }
        
        requestAnimationFrame(monitor);
    };
    
    requestAnimationFrame(monitor);
}

// 根據FPS動態調整
adjustStrategyByFPS(fps) {
    if (fps < 30) {
        // 降低渲染質量
        this.frameInterval = 100; // 10fps
        this.batchSize = 50;
    } else if (fps > 50) {
        // 提高渲染質量
        this.frameInterval = 33; // 30fps
        this.batchSize = 150;
    }
}

四、效果對比

優化前:

  • 3萬數據開始卡頓
  • 內存佔用500MB+
  • 縮放操作延遲明顯
  • 動畫掉幀嚴重

優化後:

  • 12萬數據流暢運行
  • 內存控制在200MB以內
  • 縮放操作流暢
  • 保持30fps以上動畫

五、總結

通過分層策略優化,我們成功實現了:

  1. 智能顯示:根據縮放級別動態調整顯示策略
  2. 性能平衡:在視覺效果和性能之間找到平衡點
  3. 內存控制:有效管理大量數據的內存佔用
  4. 流暢動畫:保持高幀率的動畫效果

這種分層策略不僅適用於地圖可視化,也可以擴展到其他大規模數據可視化場景中。關鍵思想是:不同視角需要不同精度的數據展示

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.