关于 mapbox-web-performance-patterns
为 Mapbox 应用开发提供性能优化指导,面向构建交互式地图的开发者。内容按用户体验影响排序,涵盖加载、渲染、数据规模与资源管理。
主要功能
- 01并行加载地图与数据
- 02优化初始包体和视口
- 03按数据量选择标记渲染方式
- 04延后非关键地图图层
- 05涵盖事件、内存与移动端优化
适用场景
- 01缩短地图首次交互等待
- 02处理大量地图标记
- 03为大型数据集选择加载方式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mapbox/ mapbox-agent-skills --skill mapbox-web-performance-patterns安装后在 Claude Code 中输入/mapbox-web-performance-patterns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
AGENTS.mdevals/evals.jsonreferences/data-loading.mdinteractions.mdlayers-styles.mdmemory.mdmobile.md详细介绍
Mapbox 性能模式技能
此技能提供性能优化指南,帮助构建快速、高效的 Mapbox 应用。模式按对用户体验的影响排序,优先介绍最关键的改进。
性能理念: 这些并非微优化。它们会表现为等待时间、卡顿,以及影响每次用户会话的重复成本。
优先级
性能问题按其对用户体验的影响排序:
- 🔴 严重(优先修复):直接导致初始加载缓慢或明显卡顿
- 🟡 高影响:造成明显延迟或资源使用增加
- 🟢 优化:逐步改进,提升完善度
🔴 严重:消除初始化瀑布流
问题: 顺序加载会造成连锁延迟,每个资源都要等待前一个资源加载完成。
注意: 现代打包工具(Vite、Webpack 等)和 ESM 动态导入会自动处理代码分割和库加载。需要消除的主要瀑布流是数据加载——也就是顺序获取地图数据,而不是与地图初始化并行进行。
反模式:顺序加载数据
// ❌ 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() });
});
}
时间线: 地图初始化(0.5s)→ 数据获取(1s)= 总计 1.5s
解决方案:并行加载数据
// ✅ 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'
});
});
}
时间线: 地图初始化与数据获取的较长耗时 = 总计约 1s
设置精确的初始视口
// ✅ Set exact center/zoom so the map fetches the right tiles immediately
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 13
});
// Use 'idle' to know when the initial viewport is fully rendered
// (all tiles, sprites, and other resources are loaded; no transitions in progress)
map.once('idle', () => {
console.log('Initial viewport fully rendered');
});
如果你知道用户最先会看到的确切区域,预先设置 center 和 zoom,即可避免地图先显示默认视图,再平移或缩放到目标位置,从而避免浪费瓦片请求。
延迟加载非关键功能
// ✅ 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
// Note: Standard style 3D buildings can be toggled via config:
// map.setConfigProperty('basemap', 'show3dObjects', false);
requestIdleCallback(
() => {
addTerrain(map);
addCustom3DLayers(map); // For classic styles with custom fill-extrusion layers
},
{ timeout: 2000 }
);
// 3. Defer analytics and non-visual features
setTimeout(() => {
initializeAnalytics(map);
}, 3000);
});
影响: 大幅缩短可交互时间,尤其是在延迟加载地形和 3D 图层时
🔴 严重:优化初始包体积
问题: 包体积过大会延长慢速网络下的可交互时间。
注意: 现代打包工具(Vite、Webpack 等)会自动为基于框架的应用处理代码分割。以下建议主要适用于优化打包内容及其加载时机。
样式 JSON 对包体积的影响
// ❌ 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
});
影响: 将内联样式改为托管样式后,初始包体积可减少 30-50%
🟡 高影响:优化标记数量
问题: 标记过多会导致渲染缓慢、交互延迟。
性能阈值
- 少于 100 个标记:可以使用 HTML 标记(Marker 类)
- 100-10,000 个标记:使用符号图层(由 GPU 加速)
- 10,000 个以上标记:建议使用聚类
- 100,000 个以上标记:使用矢量瓦片和服务端聚类
反模式:数千个 HTML 标记
// ❌ 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);
});
结果: 5,000 个 DOM 元素、交互缓慢、内存占用高
解决方案:使用符号图层(GeoJSON)
// ✅ 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);
});
性能: 10,000 个要素可在 <100ms 内渲染
解决方案:高密度场景使用聚类
// ✅ 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 // Radius relative to tile dimensions (512 = full tile width)
});
// 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
}
});
影响: 50,000 个标记可在 60 FPS 下流畅交互
总结:性能检查清单
构建 Mapbox 应用时,请按以下顺序检查这些优化项:
🔴 严重(优先处理)
- 并行加载地图库和数据(消除瀑布流)
- 对地图代码使用动态导入(缩减初始包体积)
- 延迟加载非关键功能(地形、自定义 3D 图层、分析功能)
- 标记超过 100 个时使用符号图层(而非 HTML 标记)
- 为大型数据集实现基于视口的数据加载
🟡 高影响
- 对地图事件处理程序进行防抖/节流(地理编码输入、
moveend) - 使用图层筛选器和边界框优化 queryRenderedFeatures
- 小于 5 MB 时使用 GeoJSON,大于 20 MB 时使用矢量瓦片
- 在 SPA 清理或页面销毁时,始终调用 map.remove()
- 附加
map.on('error', …)(或显示可见的错误 UI),避免样式、瓦片或令牌故障被静默忽略 - 重用弹出框实例(不要在每次交互时都创建新实例)
- 悬停/选择状态使用要素状态,而非动态创建图层
- 聚类演示:生成足够多的点来测试聚类性能(数千个,而非几百个)
Agent 反模式:仅考虑理想路径
初版 Agent 代码通常只会创建地图,不设置 map.on('error'),也不调用 map.remove(),并且只使用少量点数据,从未测试过 cluster: true。生产级演示需要具备错误可见性、清理机制和真实的数据规模。
🟢 优化
- 使用数据驱动样式整合多个图层
- 添加针对移动设备的优化(圆形图层、禁用旋转)
- 为图层设置 minzoom/maxzoom,避免在无关缩放级别渲染
- 除非确有需要,否则不要启用 preserveDrawingBuffer 或 antialias
测量
// Measure initial load time
console.time('map-load');
map.on('load', () => {
console.timeEnd('map-load');
// isStyleLoaded() returns true when style, sources, tiles, sprites, and models are all loaded
console.log('Style 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)
目标指标:
- 交互时间: 在 3G 网络下 < 2 秒
- 帧率: 平移/缩放期间保持 60 FPS
- 内存增长: 每小时使用增长 < 10 MB
- 包大小: 初始 < 500 KB(地图延迟加载)
参考文件
有关特定主题的详细模式,请加载对应的参考文件:
references/data-loading.md— GeoJSON 与 Vector Tiles 的决策矩阵、基于视口的加载、渐进式加载、大型数据集使用 Vector Tilesreferences/interactions.md— 事件防抖/节流、优化要素查询、批量 DOM 更新references/memory.md— 地图清理模式、弹出窗口/标记复用、要素状态与动态图层references/mobile.md— 设备检测、针对移动设备优化的图层、触摸交互、构造函数选项references/layers-styles.md— 使用数据驱动样式整合图层、简化表达式、基于缩放级别的可见性