mcpField
M
mapbox-web-performance-patterns

mapbox-web-performance-patterns

github.com/mapbox開発832.5k 回インストール4 週間前に更新
リポジトリ

mapbox-web-performance-patterns について

Mapbox アプリを開発する人向けのパフォーマンス最適化スキルです。読み込み、描画、データ量、リソース管理のパターンを影響度順にまとめています。

主な機能

  1. 01地図とデータの並列読み込み
  2. 02初期バンドルとビューポートの最適化
  3. 03データ量に応じたマーカー描画方式
  4. 04重要度の低い地図レイヤーの遅延
  5. 05イベント、メモリ、モバイルの最適化

活用シーン

  1. 01地図の操作開始までの待ち時間短縮
  2. 02大量の地図マーカーの処理
  3. 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.md
evals/
evals.json
references/
data-loading.md
interactions.md
layers-styles.md
memory.md
mobile.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.5 秒)→ データ取得(1 秒)= 合計 1.5 秒

解決策:データの並列読み込み
// ✅ 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'
    });
  });
}

タイムライン: max(マップの初期化、データ取得)= 合計約 1 秒

初期ビューポートを正確に設定する
// ✅ 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);
});

結果: DOM 要素が 5,000 個になり、操作が遅く、メモリ使用量も多くなります。

解決策:シンボルレイヤーを使用する(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 個のフィーチャーを 100 ミリ秒未満で描画できます。

解決策:高密度の場合はクラスタリングする
// ✅ 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)を設定し、スタイル/タイル/トークンのエラーを見逃さないようにする
  • ポップアップのインスタンスを再利用する(操作のたびに作成しない)
  • ホバー/選択の状態には、動的レイヤーではなくフィーチャー状態を使用する
  • クラスタリングのデモでは、クラスタリングの負荷をかけられるだけのポイントを生成する(数百個ではなく数千個)
エージェントのアンチパターン:正常系のみを想定

初期段階のエージェントコードでは、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
  • メモリ増加量: 1 時間の使用あたり 10 MB 未満
  • バンドルサイズ: 初期読み込みで 500 KB 未満(マップは遅延読み込み)

リファレンスファイル

特定のトピックの詳細なパターンについては、対応するリファレンスファイルを読み込んでください。

  • references/data-loading.md — GeoJSON とベクタータイルの判断基準、ビューポートに基づく読み込み、段階的な読み込み、大規模データセット向けのベクタータイル
  • references/interactions.md — イベントのデバウンス/スロットリング、フィーチャークエリの最適化、DOM 更新の一括処理
  • references/memory.md — マップのクリーンアップパターン、ポップアップ/マーカーの再利用、動的レイヤーではなくフィーチャーの状態を使用
  • references/mobile.md — デバイス検出、モバイル向けに最適化されたレイヤー、タッチ操作、コンストラクターのオプション
  • references/layers-styles.md — データ駆動型のスタイリングによるレイヤーの統合、式の簡素化、ズームに基づく表示制御

コメント