mapbox-web-performance-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMapbox Performance Patterns Skill
Mapbox性能优化技巧
This skill provides performance optimization guidance for building fast, efficient Mapbox applications. Patterns are prioritized by impact on user experience, starting with the most critical improvements.
Performance philosophy: These aren't micro-optimizations. They show up as waiting time, jank, and repeat costs that hit every user session.
本技巧为构建快速、高效的Mapbox应用提供性能优化指导,所有优化模式均按对用户体验的影响优先级排序,从最关键的改进措施开始。
性能优化理念: 这些不是微优化,它们解决的是每个用户会话都会遇到的等待时间、卡顿及重复资源消耗问题。
Priority Levels
优先级等级
Performance issues are prioritized by their impact on user experience:
- 🔴 Critical (Fix First): Directly causes slow initial load or visible jank
- 🟡 High Impact: Noticeable delays or increased resource usage
- 🟢 Optimization: Incremental improvements for polish
性能问题按对用户体验的影响分为以下优先级:
- 🔴 关键(优先修复):直接导致初始加载缓慢或明显卡顿
- 🟡 高影响:造成可感知的延迟或资源使用增加
- 🟢 优化项:用于打磨体验的渐进式改进
🔴 Critical: Eliminate Initialization Waterfalls
🔴 关键:消除初始化请求瀑布
Problem: Sequential loading creates cascading delays where each resource waits for the previous one.
Note: Modern bundlers (Vite, Webpack, etc.) and ESM dynamic imports automatically handle code splitting and library loading. The primary waterfall to eliminate is data loading - fetching map data sequentially instead of in parallel with map initialization.
问题: 顺序加载会导致级联延迟,每个资源都要等待前一个资源加载完成。
注意: 现代打包工具(Vite、Webpack等)和ESM动态导入会自动处理代码分割和库加载。需要消除的主要请求瀑布是数据加载——即地图初始化完成后再顺序获取地图数据,而非并行加载。
Anti-Pattern: Sequential Data Loading
反模式:顺序数据加载
javascript
// ❌ BAD: Data loads AFTER map initializes
async function initMap() {
const map = new mapboxgl.Map({
container: 'map',
accessToken: MAPBOX_TOKEN,
style: 'mapbox://styles/mapbox/streets-v12'
});
// Wait for map to load, THEN fetch data
map.on('load', async () => {
const data = await fetch('/api/data'); // Waterfall!
map.addSource('data', { type: 'geojson', data: await data.json() });
});
}Timeline: Map init (0.5s) → Data fetch (1s) = 1.5s total
javascript
// ❌ 错误:地图初始化完成后才加载数据
async function initMap() {
const map = new mapboxgl.Map({
container: 'map',
accessToken: MAPBOX_TOKEN,
style: 'mapbox://styles/mapbox/streets-v12'
});
// 等待地图加载完成,再获取数据
map.on('load', async () => {
const data = await fetch('/api/data'); // 请求瀑布!
map.addSource('data', { type: 'geojson', data: await data.json() });
});
}时间线: 地图初始化(0.5秒)→ 数据获取(1秒)= 总计1.5秒
✅ Solution: Parallel Data Loading
✅ 解决方案:并行数据加载
javascript
// ✅ GOOD: Data fetch starts immediately
async function initMap() {
// Start data fetch immediately (don't wait for map)
const dataPromise = fetch('/api/data').then((r) => r.json());
const map = new mapboxgl.Map({
container: 'map',
accessToken: MAPBOX_TOKEN,
style: 'mapbox://styles/mapbox/streets-v12'
});
// Data is ready when map loads
map.on('load', async () => {
const data = await dataPromise;
map.addSource('data', { type: 'geojson', data });
map.addLayer({
id: 'data-layer',
type: 'circle',
source: 'data'
});
});
}Timeline: Max(map init, data fetch) = ~1s total
javascript
// ✅ 正确:立即开始获取数据(不等待地图加载)
async function initMap() {
// 立即发起数据请求(不等待地图)
const dataPromise = fetch('/api/data').then((r) => r.json());
const map = new mapboxgl.Map({
container: 'map',
accessToken: MAPBOX_TOKEN,
style: 'mapbox://styles/mapbox/streets-v12'
});
// 地图加载完成时,数据已准备就绪
map.on('load', async () => {
const data = await dataPromise;
map.addSource('data', { type: 'geojson', data });
map.addLayer({
id: 'data-layer',
type: 'circle',
source: 'data'
});
});
}时间线: Max(地图初始化时间, 数据获取时间) = ~1秒总计
Preload Critical Tiles
预加载关键瓦片
javascript
// ✅ Preload tiles for initial viewport
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 13,
// Preload tiles 1 zoom level up
maxBounds: [
[-122.5, 37.7], // Southwest
[-122.3, 37.85] // Northeast
]
});
// Prefetch tiles before user interaction
map.once('idle', () => {
// Map is ready, tiles are cached
console.log('Initial tiles loaded');
});javascript
// ✅ 预加载初始视口的瓦片
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 13,
// 预加载高一级缩放级别的瓦片
maxBounds: [
[-122.5, 37.7], // 西南角
[-122.3, 37.85] // 东北角
]
});
// 用户交互前预获取瓦片
map.once('idle', () => {
// 地图就绪,瓦片已缓存
console.log('初始瓦片加载完成');
});Defer Non-Critical Features
延迟加载非关键功能
javascript
// ✅ Load critical features first, defer others
const map = new mapboxgl.Map({
/* config */
});
map.on('load', () => {
// 1. Add critical layers immediately
addCriticalLayers(map);
// 2. Defer secondary features (classic styles only)
// Note: 3D buildings cannot be deferred with Mapbox Standard style
requestIdleCallback(
() => {
add3DBuildings(map); // For classic styles only
addTerrain(map);
},
{ timeout: 2000 }
);
// 3. Defer analytics and non-visual features
setTimeout(() => {
initializeAnalytics(map);
}, 3000);
});Impact: Reduces time-to-interactive by 50-70%
javascript
// ✅ 先加载关键功能,延迟加载其他功能
const map = new mapboxgl.Map({
/* 配置项 */
});
map.on('load', () => {
// 1. 立即添加关键图层
addCriticalLayers(map);
// 2. 延迟加载次要功能(仅适用于经典样式)
// 注意:Mapbox Standard样式无法延迟加载3D建筑
requestIdleCallback(
() => {
add3DBuildings(map); // 仅适用于经典样式
addTerrain(map);
},
{ timeout: 2000 }
);
// 3. 延迟加载分析及非可视化功能
setTimeout(() => {
initializeAnalytics(map);
}, 3000);
});效果: 将可交互时间缩短50-70%
🔴 Critical: Optimize Initial Bundle Size
🔴 关键:优化初始包体积
Problem: Large bundles delay time-to-interactive on slow networks.
Note: Modern bundlers (Vite, Webpack, etc.) automatically handle code splitting for framework-based applications. The guidance below is most relevant for optimizing what gets bundled and when.
问题: 大包体积会导致慢网络下的可交互时间延迟。
注意: 现代打包工具(Vite、Webpack等)会自动处理框架应用的代码分割。以下指导主要针对优化打包内容及时机。
Style JSON Bundle Impact
样式JSON对包体积的影响
javascript
// ❌ BAD: Inline massive style JSON (can be 500+ KB)
const style = {
version: 8,
sources: {
/* 100s of lines */
},
layers: [
/* 100s of layers */
]
};
// ✅ GOOD: Reference Mapbox-hosted styles
const map = new mapboxgl.Map({
style: 'mapbox://styles/mapbox/streets-v12' // Fetched on demand
});
// ✅ OR: Store large custom styles externally
const map = new mapboxgl.Map({
style: '/styles/custom-style.json' // Loaded separately
});Impact: Reduces initial bundle by 30-50%
javascript
// ❌ 错误:内联大型样式JSON(可能超过500KB)
const style = {
version: 8,
sources: {
/* 数百行内容 */
},
layers: [
/* 数百个图层 */
]
};
// ✅ 正确:引用Mapbox托管的样式
const map = new mapboxgl.Map({
style: 'mapbox://styles/mapbox/streets-v12' // 按需获取
});
// ✅ 或者:将大型自定义样式存储在外部
const map = new mapboxgl.Map({
style: '/styles/custom-style.json' // 单独加载
});效果: 初始包体积减少30-50%
🟡 High Impact: Optimize Marker Count
🟡 高影响:优化标记数量
Problem: Too many markers causes slow rendering and interaction lag.
问题: 标记过多会导致渲染缓慢和交互卡顿。
Performance Thresholds
性能阈值
- < 500 markers: HTML markers OK (Marker class)
- 500-100,000 markers: Use Canvas markers or simple symbols
- 100,000-250,000 markers: Clustering required
- > 250,000 markers: Server-side clustering + vector tiles
- < 500个标记:使用HTML标记(Marker类)即可
- 500-100,000个标记:使用Canvas标记或简单符号
- 100,000-250,000个标记:需要使用聚类
- > 250,000个标记:服务端聚类 + 矢量瓦片
Anti-Pattern: Thousands of HTML Markers
反模式:数千个HTML标记
javascript
// ❌ BAD: 5,000 HTML markers = 5+ second render, janky pan/zoom
restaurants.forEach((restaurant) => {
const marker = new mapboxgl.Marker()
.setLngLat([restaurant.lng, restaurant.lat])
.setPopup(new mapboxgl.Popup().setHTML(restaurant.name))
.addTo(map);
});Result: 5,000 DOM elements, slow interactions, high memory
javascript
// ❌ 错误:5000个HTML标记 = 5秒以上渲染时间,平移/缩放卡顿
restaurants.forEach((restaurant) => {
const marker = new mapboxgl.Marker()
.setLngLat([restaurant.lng, restaurant.lat])
.setPopup(new mapboxgl.Popup().setHTML(restaurant.name))
.addTo(map);
});结果: 5000个DOM元素,交互缓慢,内存占用高
✅ Solution: Use Symbol Layers (GeoJSON)
✅ 解决方案:使用符号图层(GeoJSON)
javascript
// ✅ GOOD: GPU-accelerated rendering, smooth at 10,000+ features
map.addSource('restaurants', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: restaurants.map((r) => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [r.lng, r.lat] },
properties: { name: r.name, type: r.type }
}))
}
});
map.addLayer({
id: 'restaurants',
type: 'symbol',
source: 'restaurants',
layout: {
'icon-image': 'restaurant',
'icon-size': 0.8,
'text-field': ['get', 'name'],
'text-size': 12,
'text-offset': [0, 1.5],
'text-anchor': 'top'
}
});
// Click handler (one listener for all features)
map.on('click', 'restaurants', (e) => {
const feature = e.features[0];
new mapboxgl.Popup()
.setLngLat(feature.geometry.coordinates)
.setHTML(feature.properties.name)
.addTo(map);
});Performance: 10,000 features render in <100ms
javascript
// ✅ 正确:GPU加速渲染,10000+个要素仍保持流畅
map.addSource('restaurants', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: restaurants.map((r) => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [r.lng, r.lat] },
properties: { name: r.name, type: r.type }
}))
}
});
map.addLayer({
id: 'restaurants',
type: 'symbol',
source: 'restaurants',
layout: {
'icon-image': 'restaurant',
'icon-size': 0.8,
'text-field': ['get', 'name'],
'text-size': 12,
'text-offset': [0, 1.5],
'text-anchor': 'top'
}
});
// 点击事件处理器(所有要素共用一个监听器)
map.on('click', 'restaurants', (e) => {
const feature = e.features[0];
new mapboxgl.Popup()
.setLngLat(feature.geometry.coordinates)
.setHTML(feature.properties.name)
.addTo(map);
});性能表现: 10000个要素渲染时间<100ms
✅ Solution: Clustering for High Density
✅ 解决方案:高密度场景使用聚类
javascript
// ✅ GOOD: 50,000 markers → ~500 clusters at low zoom
map.addSource('restaurants', {
type: 'geojson',
data: restaurantsGeoJSON,
cluster: true,
clusterMaxZoom: 14, // Stop clustering at zoom 15
clusterRadius: 50 // Cluster radius in pixels
});
// Cluster circle layer
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'restaurants',
filter: ['has', 'point_count'],
paint: {
'circle-color': [
'step',
['get', 'point_count'],
'#51bbd6',
100,
'#f1f075',
750,
'#f28cb1'
],
'circle-radius': ['step', ['get', 'point_count'], 20, 100, 30, 750, 40]
}
});
// Cluster count label
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'restaurants',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-size': 12
}
});
// Individual point layer
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'restaurants',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 6
}
});Impact: 50,000 markers → 60 FPS, instant interaction
javascript
// ✅ 正确:50000个标记 → 低缩放级别下约500个聚类
map.addSource('restaurants', {
type: 'geojson',
data: restaurantsGeoJSON,
cluster: true,
clusterMaxZoom: 14, // 缩放级别15时停止聚类
clusterRadius: 50 // 聚类半径(像素)
});
// 聚类圆形图层
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'restaurants',
filter: ['has', 'point_count'],
paint: {
'circle-color': [
'step',
['get', 'point_count'],
'#51bbd6',
100,
'#f1f075',
750,
'#f28cb1'
],
'circle-radius': ['step', ['get', 'point_count'], 20, 100, 30, 750, 40]
}
});
// 聚类数量标签
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'restaurants',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-size': 12
}
});
// 单个点图层
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'restaurants',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 6
}
});效果: 50000个标记 → 60 FPS,交互即时
🟡 High Impact: Optimize Data Loading Strategy
🟡 高影响:优化数据加载策略
Problem: Loading all data upfront wastes bandwidth and slows initial render.
问题: 一次性加载所有数据会浪费带宽并减慢初始渲染。
GeoJSON vs Vector Tiles Decision Matrix
GeoJSON vs 矢量瓦片决策矩阵
| Scenario | Use GeoJSON | Use Vector Tiles |
|---|---|---|
| < 5 MB data | ✅ | ❌ |
| 5-20 MB data | ⚠️ Consider | ✅ |
| > 20 MB data | ❌ | ✅ |
| Data changes frequently | ✅ | ❌ |
| Static data, global scale | ❌ | ✅ |
| Need server-side updates | ❌ | ✅ |
| 场景 | 使用GeoJSON | 使用矢量瓦片 |
|---|---|---|
| < 5 MB 数据 | ✅ | ❌ |
| 5-20 MB 数据 | ⚠️ 考虑使用 | ✅ |
| > 20 MB 数据 | ❌ | ✅ |
| 数据频繁更新 | ✅ | ❌ |
| 静态数据、全球范围 | ❌ | ✅ |
| 需要服务端更新 | ❌ | ✅ |
✅ Viewport-Based Loading (GeoJSON)
✅ 基于视口的加载(GeoJSON)
Note: This pattern is applicable when hosting GeoJSON data locally or on external servers. Mapbox-hosted data sources are already optimized for viewport-based loading.
javascript
// ✅ Only load data in current viewport
async function loadVisibleData(map) {
const bounds = map.getBounds();
const bbox = [
bounds.getWest(),
bounds.getSouth(),
bounds.getEast(),
bounds.getNorth()
].join(',');
const data = await fetch(`/api/data?bbox=${bbox}&zoom=${map.getZoom()}`);
map.getSource('data').setData(await data.json());
}
// Update on viewport change (with debounce)
let timeout;
map.on('moveend', () => {
clearTimeout(timeout);
timeout = setTimeout(() => loadVisibleData(map), 300);
});注意: 该模式适用于本地或外部服务器托管的GeoJSON数据。Mapbox托管的数据源已针对基于视口的加载进行了优化。
javascript
// ✅ 仅加载当前视口内的数据
async function loadVisibleData(map) {
const bounds = map.getBounds();
const bbox = [
bounds.getWest(),
bounds.getSouth(),
bounds.getEast(),
bounds.getNorth()
].join(',');
const data = await fetch(`/api/data?bbox=${bbox}&zoom=${map.getZoom()}`);
map.getSource('data').setData(await data.json());
}
// 视口变化时更新(带防抖)
let timeout;
map.on('moveend', () => {
clearTimeout(timeout);
timeout = setTimeout(() => loadVisibleData(map), 300);
});✅ Progressive Data Loading
✅ 渐进式数据加载
Note: This pattern is applicable when hosting GeoJSON data locally or on external servers.
javascript
// ✅ Load basic data first, add details progressively
async function loadDataProgressive(map) {
// 1. Load simplified data first (low-res)
const simplified = await fetch('/api/data?detail=low');
map.addSource('data', {
type: 'geojson',
data: await simplified.json()
});
addLayers(map);
// 2. Load full detail in background
const detailed = await fetch('/api/data?detail=high');
map.getSource('data').setData(await detailed.json());
}注意: 该模式适用于本地或外部服务器托管的GeoJSON数据。
javascript
// ✅ 先加载基础数据,渐进式添加细节
async function loadDataProgressive(map) {
// 1. 先加载简化数据(低分辨率)
const simplified = await fetch('/api/data?detail=low');
map.addSource('data', {
type: 'geojson',
data: await simplified.json()
});
addLayers(map);
// 2. 在后台加载完整细节数据
const detailed = await fetch('/api/data?detail=high');
map.getSource('data').setData(await detailed.json());
}✅ Vector Tiles for Large Datasets
✅ 大型数据集使用矢量瓦片
Note: The / optimization shown below is primarily for self-hosted vector tilesets. Mapbox-hosted tilesets have built-in optimization via Mapbox Tiling Service (MTS) recipes that handle zoom-level optimizations automatically.
minzoommaxzoomjavascript
// ✅ Server generates tiles, client loads only visible area (self-hosted tilesets)
map.addSource('large-dataset', {
type: 'vector',
tiles: ['https://api.example.com/tiles/{z}/{x}/{y}.pbf'],
minzoom: 0,
maxzoom: 14
});
map.addLayer({
id: 'large-dataset-layer',
type: 'fill',
source: 'large-dataset',
'source-layer': 'data', // Layer name in .pbf
paint: {
'fill-color': '#088',
'fill-opacity': 0.6
}
});Impact: 10 MB dataset → 500 KB per viewport, 20x faster load
注意: 以下展示的/优化主要适用于自托管的矢量瓦片集。Mapbox托管的瓦片集已通过Mapbox Tiling Service (MTS)配方进行了内置优化,可自动处理缩放级别优化。
minzoommaxzoomjavascript
// ✅ 服务端生成瓦片,客户端仅加载可见区域(自托管瓦片集)
map.addSource('large-dataset', {
type: 'vector',
tiles: ['https://api.example.com/tiles/{z}/{x}/{y}.pbf'],
minzoom: 0,
maxzoom: 14
});
map.addLayer({
id: 'large-dataset-layer',
type: 'fill',
source: 'large-dataset',
'source-layer': 'data', // .pbf中的图层名称
paint: {
'fill-color': '#088',
'fill-opacity': 0.6
}
});效果: 10 MB数据集 → 每个视口仅加载500 KB,加载速度提升20倍
🟡 High Impact: Optimize Map Interactions
🟡 高影响:优化地图交互
Problem: Unthrottled event handlers cause performance degradation.
问题: 未节流的事件处理器会导致性能下降。
Anti-Pattern: Expensive Operations on Every Event
反模式:每次事件都执行昂贵操作
javascript
// ❌ BAD: Runs 100+ times per second during pan
map.on('move', () => {
updateVisibleFeatures(); // Expensive query
fetchDataFromAPI(); // Network request
updateUI(); // DOM manipulation
});javascript
// ❌ 错误:平移期间每秒执行100+次
map.on('move', () => {
updateVisibleFeatures(); // 昂贵的查询操作
fetchDataFromAPI(); // 网络请求
updateUI(); // DOM操作
});✅ Solution: Debounce/Throttle Events
✅ 解决方案:防抖/节流事件
javascript
// ✅ GOOD: Throttle during interaction, finalize on idle
let throttleTimeout;
// Lightweight updates during move (throttled)
map.on('move', () => {
if (throttleTimeout) return;
throttleTimeout = setTimeout(() => {
updateMapCenter(); // Cheap update
throttleTimeout = null;
}, 100);
});
// Expensive operations after interaction stops
map.on('moveend', () => {
updateVisibleFeatures();
fetchDataFromAPI();
updateUI();
});javascript
// ✅ 正确:交互期间节流,空闲时执行最终操作
let throttleTimeout;
// 平移期间的轻量级更新(节流)
map.on('move', () => {
if (throttleTimeout) return;
throttleTimeout = setTimeout(() => {
updateMapCenter(); // 低成本更新
throttleTimeout = null;
}, 100);
});
// 交互停止后执行昂贵操作
map.on('moveend', () => {
updateVisibleFeatures();
fetchDataFromAPI();
updateUI();
});✅ Optimize Feature Queries
✅ 优化要素查询
javascript
// ❌ BAD: Query all features (expensive with many layers)
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point);
console.log(features); // Could be 100+ features
});
// ✅ GOOD: Query specific layers with radius
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['restaurants', 'shops'], // Only query these layers
radius: 5 // 5px radius around click point
});
if (features.length > 0) {
showPopup(features[0]);
}
});
// ✅ EVEN BETTER: Use filter to reduce results
const features = map.queryRenderedFeatures(e.point, {
layers: ['restaurants'],
filter: ['==', ['get', 'type'], 'pizza'] // Only pizza restaurants
});javascript
// ❌ 错误:查询所有要素(图层较多时性能低下)
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point);
console.log(features); // 可能返回100+个要素
});
// ✅ 正确:查询指定图层并设置半径
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['restaurants', 'shops'], // 仅查询这些图层
radius: 5 // 点击点周围5px半径
});
if (features.length > 0) {
showPopup(features[0]);
}
});
// ✅ 更优:使用过滤器减少结果
const features = map.queryRenderedFeatures(e.point, {
layers: ['restaurants'],
filter: ['==', ['get', 'type'], 'pizza'] // 仅查询披萨店
});✅ Batch DOM Updates
✅ 批量DOM更新
javascript
// ❌ BAD: Update DOM for every feature
map.on('mousemove', 'restaurants', (e) => {
e.features.forEach((feature) => {
document.getElementById(feature.id).classList.add('highlight');
});
});
// ✅ GOOD: Batch updates with requestAnimationFrame
let pendingUpdates = new Set();
let rafScheduled = false;
map.on('mousemove', 'restaurants', (e) => {
e.features.forEach((f) => pendingUpdates.add(f.id));
if (!rafScheduled) {
rafScheduled = true;
requestAnimationFrame(() => {
pendingUpdates.forEach((id) => {
document.getElementById(id).classList.add('highlight');
});
pendingUpdates.clear();
rafScheduled = false;
});
}
});Impact: 60 FPS maintained during interaction vs 15-20 FPS without optimization
javascript
// ❌ 错误:为每个要素更新DOM
map.on('mousemove', 'restaurants', (e) => {
e.features.forEach((feature) => {
document.getElementById(feature.id).classList.add('highlight');
});
});
// ✅ 正确:使用requestAnimationFrame批量更新
let pendingUpdates = new Set();
let rafScheduled = false;
map.on('mousemove', 'restaurants', (e) => {
e.features.forEach((f) => pendingUpdates.add(f.id));
if (!rafScheduled) {
rafScheduled = true;
requestAnimationFrame(() => {
pendingUpdates.forEach((id) => {
document.getElementById(id).classList.add('highlight');
});
pendingUpdates.clear();
rafScheduled = false;
});
}
});效果: 交互期间保持60 FPS,未优化时仅15-20 FPS
🟢 Optimization: Memory Management
🟢 优化项:内存管理
Problem: Memory leaks cause browser tabs to become unresponsive over time.
问题: 内存泄漏会导致浏览器标签页随时间推移变得无响应。
✅ Always Clean Up Map Resources
✅ 始终清理地图资源
javascript
// ✅ Essential cleanup pattern
function cleanupMap(map) {
if (!map) return;
// 1. Remove event listeners
map.off('load', handleLoad);
map.off('move', handleMove);
// 2. Remove layers (if adding/removing dynamically)
if (map.getLayer('dynamic-layer')) {
map.removeLayer('dynamic-layer');
}
// 3. Remove sources (if adding/removing dynamically)
if (map.getSource('dynamic-source')) {
map.removeSource('dynamic-source');
}
// 4. Remove controls
map.removeControl(navigationControl);
// 5. CRITICAL: Remove map instance
map.remove();
}
// React example
useEffect(() => {
const map = new mapboxgl.Map({
/* config */
});
return () => {
cleanupMap(map); // Called on unmount
};
}, []);javascript
// ✅ 必要的清理模式
function cleanupMap(map) {
if (!map) return;
// 1. 移除事件监听器
map.off('load', handleLoad);
map.off('move', handleMove);
// 2. 移除图层(如果动态添加/移除)
if (map.getLayer('dynamic-layer')) {
map.removeLayer('dynamic-layer');
}
// 3. 移除数据源(如果动态添加/移除)
if (map.getSource('dynamic-source')) {
map.removeSource('dynamic-source');
}
// 4. 移除控件
map.removeControl(navigationControl);
// 5. 关键:移除地图实例
map.remove();
}
// React示例
useEffect(() => {
const map = new mapboxgl.Map({
/* 配置项 */
});
return () => {
cleanupMap(map); // 卸载时调用
};
}, []);✅ Clean Up Popups and Markers
✅ 清理弹窗和标记
javascript
// ❌ BAD: Creates new popup on every click (memory leak)
map.on('click', 'restaurants', (e) => {
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML(e.features[0].properties.name)
.addTo(map);
// Popup never removed!
});
// ✅ GOOD: Reuse single popup instance
let popup = new mapboxgl.Popup({ closeOnClick: true });
map.on('click', 'restaurants', (e) => {
popup.setLngLat(e.lngLat).setHTML(e.features[0].properties.name).addTo(map);
// Popup removed when map closes or new popup shows
});
// Cleanup
function cleanup() {
popup.remove();
popup = null;
}javascript
// ❌ 错误:每次点击创建新弹窗(内存泄漏)
map.on('click', 'restaurants', (e) => {
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML(e.features[0].properties.name)
.addTo(map);
// 弹窗从未被移除!
});
// ✅ 正确:复用单个弹窗实例
let popup = new mapboxgl.Popup({ closeOnClick: true });
map.on('click', 'restaurants', (e) => {
popup.setLngLat(e.lngLat).setHTML(e.features[0].properties.name).addTo(map);
// 地图关闭或显示新弹窗时,旧弹窗会被移除
});
// 清理
function cleanup() {
popup.remove();
popup = null;
}✅ Use Feature State Instead of New Layers
✅ 使用要素状态而非创建新图层
javascript
// ❌ BAD: Create new layer for hover (memory overhead)
let hoveredFeatureId = null;
map.on('mousemove', 'restaurants', (e) => {
if (map.getLayer('hover-layer')) {
map.removeLayer('hover-layer');
}
map.addLayer({
id: 'hover-layer',
type: 'circle',
source: 'restaurants',
filter: ['==', ['id'], e.features[0].id],
paint: { 'circle-color': 'yellow' }
});
});
// ✅ GOOD: Use feature state (efficient, no layer creation)
map.on('mousemove', 'restaurants', (e) => {
if (e.features.length > 0) {
// Remove previous hover state
if (hoveredFeatureId !== null) {
map.setFeatureState(
{ source: 'restaurants', id: hoveredFeatureId },
{ hover: false }
);
}
// Set new hover state
hoveredFeatureId = e.features[0].id;
map.setFeatureState(
{ source: 'restaurants', id: hoveredFeatureId },
{ hover: true }
);
}
});
// Style uses feature state
map.addLayer({
id: 'restaurants',
type: 'circle',
source: 'restaurants',
paint: {
'circle-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
'#ffff00', // Yellow when hover
'#0000ff' // Blue otherwise
]
}
});Impact: Prevents memory growth from 200 MB → 2 GB over session
javascript
// ❌ 错误:为悬停效果创建新图层(内存开销大)
let hoveredFeatureId = null;
map.on('mousemove', 'restaurants', (e) => {
if (map.getLayer('hover-layer')) {
map.removeLayer('hover-layer');
}
map.addLayer({
id: 'hover-layer',
type: 'circle',
source: 'restaurants',
filter: ['==', ['id'], e.features[0].id],
paint: { 'circle-color': 'yellow' }
});
});
// ✅ 正确:使用要素状态(高效,无需创建图层)
map.on('mousemove', 'restaurants', (e) => {
if (e.features.length > 0) {
// 移除之前的悬停状态
if (hoveredFeatureId !== null) {
map.setFeatureState(
{ source: 'restaurants', id: hoveredFeatureId },
{ hover: false }
);
}
// 设置新的悬停状态
hoveredFeatureId = e.features[0].id;
map.setFeatureState(
{ source: 'restaurants', id: hoveredFeatureId },
{ hover: true }
);
}
});
// 样式使用要素状态
map.addLayer({
id: 'restaurants',
type: 'circle',
source: 'restaurants',
paint: {
'circle-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
'#ffff00', // 悬停时为黄色
'#0000ff' // 否则为蓝色
]
}
});效果: 避免会话期间内存占用从200 MB增长到2 GB
🟢 Optimization: Mobile Performance
🟢 优化项:移动端性能
Problem: Mobile devices have limited resources (CPU, GPU, memory, battery).
问题: 移动设备资源有限(CPU、GPU、内存、电池)。
Mobile-Specific Optimizations
移动端特定优化
javascript
// Detect mobile device
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
// Mobile optimizations
...(isMobile && {
// Reduce tile quality on mobile (30% smaller tiles)
transformRequest: (url, resourceType) => {
if (resourceType === 'Tile') {
return {
url: url.replace('@2x', '') // Use 1x tiles instead of 2x
};
}
},
// Disable expensive features on mobile
maxPitch: 45, // Limit 3D perspective (battery saver)
// Simplify rendering
fadeDuration: 100 // Faster transitions = less GPU work
})
});
// Load fewer features on mobile
map.on('load', () => {
if (isMobile) {
// Simple marker rendering
map.addLayer({
id: 'markers-mobile',
type: 'circle',
source: 'data',
paint: {
'circle-radius': 8,
'circle-color': '#007cbf'
}
});
} else {
// Rich desktop rendering with icons and labels
map.addLayer({
id: 'markers-desktop',
type: 'symbol',
source: 'data',
layout: {
'icon-image': 'marker',
'icon-size': 1,
'text-field': ['get', 'name'],
'text-size': 12,
'text-offset': [0, 1.5]
}
});
}
});javascript
// 检测移动设备
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
// 移动端优化
...(isMobile && {
// 降低移动端瓦片质量(瓦片缩小30%)
transformRequest: (url, resourceType) => {
if (resourceType === 'Tile') {
return {
url: url.replace('@2x', '') // 使用1x瓦片而非2x
};
}
},
// 禁用移动端的昂贵功能
maxPitch: 45, // 限制3D视角(节省电量)
// 简化渲染
fadeDuration: 100 // 更快的过渡 = 更少的GPU工作
})
});
// 移动端加载更少的要素
map.on('load', () => {
if (isMobile) {
// 简单的标记渲染
map.addLayer({
id: 'markers-mobile',
type: 'circle',
source: 'data',
paint: {
'circle-radius': 8,
'circle-color': '#007cbf'
}
});
} else {
// 桌面端使用带图标和标签的丰富渲染
map.addLayer({
id: 'markers-desktop',
type: 'symbol',
source: 'data',
layout: {
'icon-image': 'marker',
'icon-size': 1,
'text-field': ['get', 'name'],
'text-size': 12,
'text-offset': [0, 1.5]
}
});
}
});Touch Event Optimization
触摸事件优化
javascript
// ✅ Optimize touch interactions
map.touchZoomRotate.disableRotation(); // Disable rotation (simpler gestures)
// Debounce expensive operations during touch
let touchTimeout;
map.on('touchmove', () => {
if (touchTimeout) clearTimeout(touchTimeout);
touchTimeout = setTimeout(() => {
updateVisibleData();
}, 500); // Wait for touch to settle
});javascript
// ✅ 优化触摸交互
map.touchZoomRotate.disableRotation(); // 禁用旋转(简化手势)
// 触摸期间防抖昂贵操作
let touchTimeout;
map.on('touchmove', () => {
if (touchTimeout) clearTimeout(touchTimeout);
touchTimeout = setTimeout(() => {
updateVisibleData();
}, 500); // 等待触摸操作稳定
});Battery-Conscious Loading
考虑电量的加载策略
javascript
// ✅ Respect battery status
if ('getBattery' in navigator) {
navigator.getBattery().then((battery) => {
const isLowBattery = battery.level < 0.2;
if (isLowBattery) {
// Reduce quality and features
map.setMaxZoom(15); // Limit detail
disableAnimations(map);
disableTerrain(map);
}
});
}Impact: 50% reduction in battery drain, smoother interactions on older devices
javascript
// ✅ 尊重电池状态
if ('getBattery' in navigator) {
navigator.getBattery().then((battery) => {
const isLowBattery = battery.level < 0.2;
if (isLowBattery) {
// 降低质量并减少功能
map.setMaxZoom(15); // 限制细节
disableAnimations(map);
disableTerrain(map);
}
});
}效果: 电池消耗减少50%,旧设备上交互更流畅
🟢 Optimization: Layer and Style Performance
🟢 优化项:图层和样式性能
Consolidate Layers
合并图层
javascript
// ❌ BAD: 20 separate layers for restaurant types
restaurantTypes.forEach((type) => {
map.addLayer({
id: `restaurants-${type}`,
type: 'symbol',
source: 'restaurants',
filter: ['==', ['get', 'type'], type],
layout: { 'icon-image': `${type}-icon` }
});
});
// ✅ GOOD: Single layer with data-driven styling
map.addLayer({
id: 'restaurants',
type: 'symbol',
source: 'restaurants',
layout: {
'icon-image': [
'match',
['get', 'type'],
'pizza',
'pizza-icon',
'burger',
'burger-icon',
'sushi',
'sushi-icon',
'default-icon' // fallback
]
}
});Impact: 20 layers → 1 layer = 95% fewer draw calls
javascript
// ❌ 错误:为不同类型的餐厅创建20个独立图层
restaurantTypes.forEach((type) => {
map.addLayer({
id: `restaurants-${type}`,
type: 'symbol',
source: 'restaurants',
filter: ['==', ['get', 'type'], type],
layout: { 'icon-image': `${type}-icon` }
});
});
// ✅ 正确:使用数据驱动样式的单个图层
map.addLayer({
id: 'restaurants',
type: 'symbol',
source: 'restaurants',
layout: {
'icon-image': [
'match',
['get', 'type'],
'pizza',
'pizza-icon',
'burger',
'burger-icon',
'sushi',
'sushi-icon',
'default-icon' // 回退
]
}
});效果: 20个图层 → 1个图层 = 减少95%的绘制调用
Simplify Paint Properties
简化绘制属性
javascript
// ❌ BAD: Complex expression evaluated per frame
map.addLayer({
id: 'buildings',
type: 'fill-extrusion',
source: 'buildings',
paint: {
'fill-extrusion-color': [
'interpolate',
['linear'],
['get', 'height'],
0,
'#dedede',
10,
'#c0c0c0',
20,
'#a0a0a0',
50,
'#808080',
100,
'#606060'
],
'fill-extrusion-height': [
'*',
['get', 'height'],
['case', ['>', ['zoom'], 16], 1.5, 1.0]
]
}
});
// ✅ GOOD: Pre-compute where possible
// Pre-process data to add computed properties
const buildingsWithPrecomputed = {
type: 'FeatureCollection',
features: buildings.features.map((f) => ({
...f,
properties: {
...f.properties,
displayHeight: f.properties.height * 1.5, // Pre-computed
heightColor: getColorForHeight(f.properties.height) // Pre-computed
}
}))
};
map.addLayer({
id: 'buildings',
type: 'fill-extrusion',
paint: {
'fill-extrusion-color': ['get', 'heightColor'],
'fill-extrusion-height': ['get', 'displayHeight']
}
});javascript
// ❌ 错误:每帧都计算复杂表达式
map.addLayer({
id: 'buildings',
type: 'fill-extrusion',
source: 'buildings',
paint: {
'fill-extrusion-color': [
'interpolate',
['linear'],
['get', 'height'],
0,
'#dedede',
10,
'#c0c0c0',
20,
'#a0a0a0',
50,
'#808080',
100,
'#606060'
],
'fill-extrusion-height': [
'*',
['get', 'height'],
['case', ['>', ['zoom'], 16], 1.5, 1.0]
]
}
});
// ✅ 正确:尽可能预计算
// 预处理数据以添加计算属性
const buildingsWithPrecomputed = {
type: 'FeatureCollection',
features: buildings.features.map((f) => ({
...f,
properties: {
...f.properties,
displayHeight: f.properties.height * 1.5, // 预计算
heightColor: getColorForHeight(f.properties.height) // 预计算
}
}))
};
map.addLayer({
id: 'buildings',
type: 'fill-extrusion',
paint: {
'fill-extrusion-color': ['get', 'heightColor'],
'fill-extrusion-height': ['get', 'displayHeight']
}
});Use Zoom-Based Layer Visibility
使用基于缩放级别的图层可见性
javascript
// ✅ Only render layers when visible
map.addLayer({
id: 'building-details',
type: 'fill',
source: 'buildings',
minzoom: 15, // Only render at zoom 15+
maxzoom: 22,
paint: { 'fill-color': '#aaa' }
});
map.addLayer({
id: 'poi-labels',
type: 'symbol',
source: 'pois',
minzoom: 12, // Hide at low zoom levels
layout: {
'text-field': ['get', 'name'],
visibility: 'visible'
}
});Impact: 40% reduction in GPU usage at low zoom levels
javascript
// ✅ 仅在可见时渲染图层
map.addLayer({
id: 'building-details',
type: 'fill',
source: 'buildings',
minzoom: 15, // 仅在缩放级别15+时渲染
maxzoom: 22,
paint: { 'fill-color': '#aaa' }
});
map.addLayer({
id: 'poi-labels',
type: 'symbol',
source: 'pois',
minzoom: 12, // 低缩放级别时隐藏
layout: {
'text-field': ['get', 'name'],
visibility: 'visible'
}
});效果: 低缩放级别下GPU使用率减少40%
Summary: Performance Checklist
总结:性能优化检查清单
When building a Mapbox application, verify these optimizations in order:
构建Mapbox应用时,按以下顺序验证这些优化措施:
🔴 Critical (Do First)
🔴 关键(优先完成)
- Load map library and data in parallel (eliminate waterfalls)
- Use dynamic imports for map code (reduce initial bundle)
- Defer non-critical features (3D, terrain, analytics)
- Use clustering or symbol layers for > 100 markers
- Implement viewport-based data loading for large datasets
- 并行加载地图库和数据(消除请求瀑布)
- 对地图代码使用动态导入(减少初始包体积)
- 延迟加载非关键功能(3D、地形、分析)
- 对>100个标记使用聚类或符号图层
- 对大型数据集实现基于视口的数据加载
🟡 High Impact
🟡 高影响
- Debounce/throttle map event handlers
- Optimize queryRenderedFeatures with layers and radius
- Use GeoJSON for < 1 MB, vector tiles for > 10 MB
- Implement progressive data loading
- 防抖/节流地图事件处理器
- 使用layers和radius优化queryRenderedFeatures
- <1 MB数据使用GeoJSON,>10 MB数据使用矢量瓦片
- 实现渐进式数据加载
🟢 Optimization
🟢 优化项
- Always call map.remove() on cleanup
- Reuse popup instances (don't create on every interaction)
- Use feature state instead of dynamic layers
- Consolidate multiple layers with data-driven styling
- Add mobile-specific optimizations (simpler rendering, battery awareness)
- Set minzoom/maxzoom on layers to avoid rendering when not visible
- 清理时始终调用map.remove()
- 复用弹窗实例(不要每次交互都创建新弹窗)
- 使用要素状态而非动态创建图层
- 使用数据驱动样式合并多个图层
- 添加移动端特定优化(简化渲染、电量感知)
- 为图层设置minzoom/maxzoom以避免不必要的渲染
Measurement
性能测量
Use these tools to measure impact:
javascript
// Measure initial load time
console.time('map-load');
map.on('load', () => {
console.timeEnd('map-load');
console.log('Tiles loaded:', map.isStyleLoaded());
});
// Monitor frame rate
let frameCount = 0;
map.on('render', () => frameCount++);
setInterval(() => {
console.log('FPS:', frameCount);
frameCount = 0;
}, 1000);
// Check memory usage (Chrome DevTools → Performance → Memory)Target metrics:
- Time to Interactive: < 2 seconds on 3G
- Frame Rate: 60 FPS during pan/zoom
- Memory Growth: < 10 MB per hour of usage
- Bundle Size: < 500 KB initial (map lazy-loaded)
使用以下工具测量优化效果:
javascript
// 测量初始加载时间
console.time('map-load');
map.on('load', () => {
console.timeEnd('map-load');
console.log('瓦片加载完成:', map.isStyleLoaded());
});
// 监控帧率
let frameCount = 0;
map.on('render', () => frameCount++);
setInterval(() => {
console.log('FPS:', frameCount);
frameCount = 0;
}, 1000);
// 检查内存使用(Chrome开发者工具 → 性能 → 内存)目标指标:
- 可交互时间: 3G网络下<2秒
- 帧率: 平移/缩放期间保持60 FPS
- 内存增长: 每小时使用增长<10 MB
- 包体积: 初始包<500 KB(地图懒加载)