mcpField
A
arcgis-performance

arcgis-performance

github.com/saschabrunnerch開発2352 回インストール29 週間前に更新
リポジトリ

arcgis-performance について

arcgis-performance は、ArcGIS Maps SDK for JavaScript の 2D MapView と 3D SceneView を最適化する開発者向けスキルです。準備完了の扱い、読み込み、描画、バンドルサイズ、メモリ管理を扱います。

主な機能

  1. 01ビューとレイヤーの準備完了処理
  2. 02レイヤーの並列読み込み
  3. 03必要なモジュールの個別インポート
  4. 04フィーチャーのクエリと描画の最適化
  5. 05ビュー、監視処理、クエリの後処理

活用シーン

  1. 01マップ初期化の待ち時間削減
  2. 02大規模フィーチャーデータの効率的な処理
  3. 03シングルページアプリのメモリリーク防止

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/saschabrunnerch/arcgis-maps-sdk-js-ai-context --skill arcgis-performance

インストール後、Claude Code で/arcgis-performanceを入力して読み込まれたか確認します。

詳細

ArcGIS のパフォーマンス

ArcGIS Maps SDK for JavaScript アプリケーションの読み込み時間の短縮、メモリ使用量の削減、効率的なデータ処理、および 2D の MapView と 3D の SceneView の両方でスムーズなレンダリングを実現するために最適化する場合に、このスキルを使用してください。

最優先(P0)

マップの初期化

不適切な準備完了パターンを使用すると、競合状態、イベントの取りこぼし、CPU サイクルの無駄が発生します。

マップコンポーネント
// Anti-pattern: polling or setTimeout to check view readiness
const mapElement = document.querySelector("arcgis-map");
const checkReady = setInterval(() => {
  if (mapElement.view && mapElement.view.ready) {
    clearInterval(checkReady);
    initializeApp(mapElement.view);
  }
}, 100);
// Correct: use viewOnReady() which returns a promise
const mapElement = document.querySelector("arcgis-map");
await mapElement.viewOnReady();
initializeApp(mapElement.view);

影響: ポーリングは CPU サイクルを浪費し、予測できない遅延を引き起こします。viewOnReady() はビューが使用可能になった最も早い時点で解決され、オーバーヘッドはゼロです。

コア API
// Anti-pattern: setTimeout to "wait" for the view
const view = new MapView({ container: "viewDiv", map });
setTimeout(() => {
  console.log("Extent:", view.extent); // May still be undefined
}, 3000);
// Correct: use view.when() which resolves when the view is ready
const view = new MapView({ container: "viewDiv", map });
await view.when();
console.log("Extent:", view.extent);

影響: setTimeout は実行が早すぎる(ビューの準備ができていない)か、遅すぎる(アイドル時間が無駄になる)かのどちらかです。view.when() は適切なタイミングで解決されます。

レイヤーの準備完了
// Anti-pattern: accessing layer properties before it has loaded
const layer = new FeatureLayer({ url: serviceUrl });
map.add(layer);
console.log(layer.fields); // undefined
// Correct: wait for the layer to load
const layer = new FeatureLayer({ url: serviceUrl });
map.add(layer);
await layer.when();
console.log(layer.fields); // Now available

影響: 読み込みが完了していないレイヤーのプロパティにアクセスすると、undefined または古いデータが返されます。

データ読み込みのウォーターフォール
// Anti-pattern: sequential layer loading creates a waterfall
for (const url of urls) {
  const layer = new FeatureLayer({ url });
  map.add(layer);
  await layer.when(); // Blocks until this layer loads before starting the next
}
// Correct: parallel layer loading with Promise.all
const layers = urls.map((url) => new FeatureLayer({ url }));
map.addMany(layers);
await Promise.all(layers.map((layer) => layer.when()));

影響: それぞれ 500ms かかるレイヤーが 4 つある場合、逐次読み込みには約 2000ms かかります。並列読み込みなら約 500ms です。

エラー処理を伴う並列読み込み
// Correct: parallel loading with individual error handling
const layers = urls.map((url) => new FeatureLayer({ url }));
map.addMany(layers);

const results = await Promise.allSettled(layers.map((layer) => layer.when()));
results.forEach((result, index) => {
  if (result.status === "rejected") {
    console.warn(`Layer ${index} failed to load:`, result.reason);
    map.remove(layers[index]);
  }
});

影響: Promise.allSettled を使用すると、1 つのレイヤーの読み込み失敗によって他のすべてのレイヤーがブロックされるのを防げます。

バンドルサイズ
コア API のインポート
// Anti-pattern: barrel imports pull in the entire module tree
import { Map, MapView } from "@arcgis/core";
import { FeatureLayer, GraphicsLayer } from "@arcgis/core/layers";
// Correct: deep imports enable tree-shaking
import Map from "@arcgis/core/Map.js";
import MapView from "@arcgis/core/views/MapView.js";
import FeatureLayer from "@arcgis/core/layers/FeatureLayer.js";

影響: バレルインポートはツリーシェイキングを回避し、未使用のコードを数百キロバイト単位でバンドルに追加する場合があります。

マップコンポーネントのインポート
// Anti-pattern: importing the entire map-components package
import "@arcgis/map-components";
// Correct: import only the components you use
import "@arcgis/map-components/dist/components/arcgis-map";
import "@arcgis/map-components/dist/components/arcgis-zoom";
import "@arcgis/map-components/dist/components/arcgis-legend";

影響: パッケージ全体をインポートすると、使用するコンポーネントが一部だけでも、すべてのコンポーネントが登録されます。

バンドルサイズの比較
インポートパターンバンドルへのおおよその影響
import "@arcgis/map-components"コンポーネントライブラリ全体を読み込み
import "@arcgis/map-components/dist/components/arcgis-map"マップコンポーネントと依存関係のみ
import { Map } from "@arcgis/core"バレルインポートによりコア全体を読み込み
import Map from "@arcgis/core/Map.js"Map クラスと直接の依存関係のみ
コード分割のための動的インポート
// Anti-pattern: importing heavy modules at startup
import Print from "@arcgis/core/widgets/Print.js";
import Sketch from "@arcgis/core/widgets/Sketch.js";
import Editor from "@arcgis/core/widgets/Editor.js";
// Correct: dynamic import loads modules only when needed
async function addPrintWidget(view) {
  const { default: Print } = await import("@arcgis/core/widgets/Print.js");
  const print = new Print({ view });
  view.ui.add(print, "top-right");
}

影響: Print、Sketch、Editor などのウィジェットはサイズが大きくなります。動的インポートを使えば、これらを初期バンドルから除外し、操作可能になるまでの時間を短縮できます。

効果大(P1)

FeatureLayer のクエリ最適化
outFields を指定する
// Anti-pattern: requesting all fields
const results = await layer.queryFeatures({
  where: "status = 'active'",
  outFields: ["*"],
  returnGeometry: true,
});
// Correct: request only the fields you need
const results = await layer.queryFeatures({
  where: "status = 'active'",
  outFields: ["OBJECTID", "name", "status", "category"],
  returnGeometry: true,
});

影響: フィールドが 50 個あるレイヤーで 4 個だけを要求すると、ペイロードサイズを約 90% 削減できます。

不要な場合はジオメトリを省略する
// Anti-pattern: fetching geometry for a list view
const results = await layer.queryFeatures({
  where: "population > 100000",
  outFields: ["name", "population"],
  returnGeometry: true, // Default is true
});
// Only using attributes
results.features.forEach((f) => addToList(f.attributes.name));
// Correct: disable geometry when only attributes are needed
const results = await layer.queryFeatures({
  where: "population > 100000",
  outFields: ["name", "population"],
  returnGeometry: false,
});

影響: ジオメトリデータ(特にポリゴン)は、属性データより桁違いに大きくなる場合があります。

definitionExpression によるサーバー側フィルタリング
// Anti-pattern: loading all features then filtering on the client
const allResults = await layer.queryFeatures({
  where: "1=1",
  outFields: ["*"],
});
const filtered = allResults.features.filter(
  (f) => f.attributes.region === "West" && f.attributes.revenue > 50000,
);
// Correct: use definitionExpression for server-side filtering
const layer = new FeatureLayer({
  url: serviceUrl,
  definitionExpression: "region = 'West' AND revenue > 50000",
  outFields: ["OBJECTID", "name", "region", "revenue"],
});

影響: 500 件に絞り込むために 100,000 件のフィーチャを読み込むと、帯域幅、メモリ、CPU を浪費します。

軽量なクエリメソッドを使用する
// Anti-pattern: querying full features just to get a count
const results = await layer.queryFeatures({ where: "status = 'active'" });
const count = results.features.length;
// Correct: use queryFeatureCount for count-only operations
const count = await layer.queryFeatureCount({ where: "status = 'active'" });

// Correct: use queryExtent when you only need the bounding box
const { extent } = await layer.queryExtent({ where: "status = 'active'" });
await view.goTo(extent);

影響: queryFeatureCount と queryExtent は、軽量なサーバー操作です。

大規模データセットの処理
戦略のしきい値
フィーチャ数戦略実装
< 2,000そのまま描画デフォルトの描画
2,000 - 50,000クラスタリングfeatureReduction のタイプ "cluster"
50,000 - 500,000ビニングfeatureReduction のタイプ "binning"
500,000+サーバー側のタイル化VectorTileLayer またはサーバー側フィルタリング
クラスタリング(2,000 - 50,000 フィーチャ)
// Anti-pattern: rendering thousands of individual point features
const layer = new FeatureLayer({ url: serviceUrl });
// Correct: enable clustering to group nearby features
const layer = new FeatureLayer({
  url: serviceUrl,
  featureReduction: {
    type: "cluster",
    clusterRadius: "100px",
    clusterMinSize: "24px",
    clusterMaxSize: "60px",
    labelingInfo: [
      {
        deconflictionStrategy: "none",
        labelExpressionInfo: {
          expression: "Text($feature.cluster_count, '#,###')",
        },
        symbol: {
          type: "text",
          color: "#004a5d",
          font: { size: "12px", weight: "bold" },
        },
        labelPlacement: "center-center",
      },
    ],
  },
});

影響: クラスタリングにより、描画される要素を数千個から数十個に減らし、フレームレートを大幅に向上できます。

ビニング(50,000 - 500,000 フィーチャ)
const layer = new FeatureLayer({
  url: serviceUrl,
  featureReduction: {
    type: "binning",
    fixedBinLevel: 6,
    labelsVisible: true,
    labelingInfo: [
      {
        deconflictionStrategy: "none",
        labelExpressionInfo: {
          expression: "Text($feature.aggregateCount, '#,###')",
        },
        symbol: {
          type: "text",
          color: "white",
          font: { size: "10px", weight: "bold" },
        },
      },
    ],
    renderer: {
      type: "simple",
      symbol: {
        type: "simple-fill",
        color: [0, 76, 115, 0.5],
        outline: { color: "white", width: 0.5 },
      },
      visualVariables: [
        {
          type: "color",
          field: "aggregateCount",
          stops: [
            { value: 1, color: "#d7e1ee" },
            { value: 100, color: "#6baed6" },
            { value: 1000, color: "#08519c" },
          ],
        },
      ],
    },
  },
});

影響: ビニングでは、サーバー上でフィーチャを六角形のビンに集約し、データ転送量と描画コストを削減します。

サーバー側タイリング(500,000 件以上のフィーチャ)
import VectorTileLayer from "@arcgis/core/layers/VectorTileLayer.js";

const layer = new VectorTileLayer({ url: vectorTileServerUrl });
map.add(layer);

// Alternative: server-side filtering to limit features displayed
const filteredLayer = new FeatureLayer({
  url: serviceUrl,
  definitionExpression: "population > 10000",
  maxScale: 50000,
});
メモリー管理
ビューの破棄
// Anti-pattern: removing DOM element without destroying view
function removeMap() {
  document.getElementById("viewDiv").remove();
  // View still alive in memory
}
// Correct: destroy the view to release all resources
function removeMap(view) {
  view.destroy();
}

影響: 破棄されていない MapView は、1 つで 50~200MB のメモリーを保持することがあります。SPA ではメモリー使用量が増え続け、最終的にタブがクラッシュします。

ハンドルグループによるハンドルのクリーンアップ
// Anti-pattern: creating watchers without tracking them
function setupWatchers(view) {
  reactiveUtils.watch(
    () => view.extent,
    (extent) => updatePanel(extent),
  );
  reactiveUtils.watch(
    () => view.scale,
    (scale) => updateScaleBar(scale),
  );
  // These watches live forever
}
// Correct: use handle groups for organized cleanup
import * as reactiveUtils from "@arcgis/core/core/reactiveUtils.js";

function setupWatchers(view) {
  const handle1 = reactiveUtils.watch(
    () => view.extent,
    (extent) => updatePanel(extent),
  );
  const handle2 = reactiveUtils.watch(
    () => view.scale,
    (scale) => updateScaleBar(scale),
  );
  view.addHandles([handle1, handle2], "my-watchers");
}

function cleanup(view) {
  view.removeHandles("my-watchers");
}

影響: 孤立したウォッチャーは、破棄されたコンポーネントでコールバックを実行し続け、エラーやメモリーリークの原因になります。

キャンセル可能なクエリーのための AbortController
// Anti-pattern: queries that cannot be cancelled
async function onViewChange(view) {
  const results = await layer.queryFeatures({
    geometry: view.extent,
    outFields: ["name"],
  });
  currentResults = results;
}
// Correct: use AbortController to cancel superseded queries
import * as promiseUtils from "@arcgis/core/core/promiseUtils.js";

let abortController = null;

async function onViewChange(view) {
  if (abortController) abortController.abort();
  abortController = new AbortController();

  try {
    const results = await layer.queryFeatures(
      { geometry: view.extent, outFields: ["name"] },
      { signal: abortController.signal },
    );
    updateUI(results);
  } catch (error) {
    if (!promiseUtils.isAbortError(error)) {
      console.error("Query failed:", error);
    }
  }
}

影響: キャンセルしないと、ビューを頻繁に変更した際に、数十件のクエリーが同時に実行されます。

React のクリーンアップパターン
// Anti-pattern: no cleanup in React component
function MapComponent() {
  const mapRef = useRef(null);
  useEffect(() => {
    const view = new MapView({
      container: mapRef.current,
      map: new Map({ basemap: "streets-vector" }),
    });
    // Missing cleanup
  }, []);
  return <div ref={mapRef} style={{ height: "100%" }} />;
}
// Correct: destroy view on unmount
function MapComponent() {
  const mapRef = useRef(null);
  useEffect(() => {
    const view = new MapView({
      container: mapRef.current,
      map: new Map({ basemap: "streets-vector" }),
    });
    return () => {
      view.destroy();
    };
  }, []);
  return <div ref={mapRef} style={{ height: "100%" }} />;
}

影響: React の Strict Mode では、コンポーネントが 2 回マウントおよびアンマウントされます。クリーンアップしないと、ビューが 2 つ作成される一方、表示されるのは 1 つだけです。

注: Map Components(<arcgis-map>、<arcgis-scene>)は独自のライフサイクルを管理します。React、Angular、または Vue によって DOM 要素が削除されると、コンポーネントは内部でクリーンアップを実行します。

2D ビューのパフォーマンス
停止状態を待機する
// Anti-pattern: running expensive queries on every view change
reactiveUtils.watch(
  () => view.extent,
  async (extent) => {
    // Fires dozens of times per second during panning
    const results = await layer.queryFeatures({
      geometry: extent,
      outFields: ["name", "population"],
    });
    updateSidebar(results);
  },
);
// Correct: wait for view.stationary before querying
import * as reactiveUtils from "@arcgis/core/core/reactiveUtils.js";

reactiveUtils.watch(
  () => view.stationary,
  async (isStationary) => {
    if (isStationary) {
      const results = await layer.queryFeatures({
        geometry: view.extent,
        outFields: ["name", "population"],
      });
      updateSidebar(results);
    }
  },
);

影響: view.stationary が true になるのは、ユーザーが操作を停止した後だけです。操作ごとのクエリー呼び出しを数百回から 1 回に減らします。

クエリーのデバウンス
import * as promiseUtils from "@arcgis/core/core/promiseUtils.js";

const debouncedQuery = promiseUtils.debounce(async (extent) => {
  const results = await layer.queryFeatures({
    geometry: extent,
    outFields: ["name"],
  });
  updateUI(results);
});

reactiveUtils.watch(
  () => view.extent,
  (extent) => debouncedQuery(extent),
);

影響: promiseUtils.debounce は ArcGIS に対応しており、中止エラーを処理し、最後に呼び出された処理だけを実行します。

縮尺に応じたレイヤーの表示
// Anti-pattern: all layers visible at all zoom levels
const parcelsLayer = new FeatureLayer({ url: parcelsUrl });
// Correct: use minScale and maxScale
const parcelsLayer = new FeatureLayer({
  url: parcelsUrl,
  minScale: 25000, // Only visible when zoomed in past 1:25,000
  maxScale: 0,
});

const regionsLayer = new FeatureLayer({
  url: regionsUrl,
  minScale: 0,
  maxScale: 50000, // Hidden when zoomed in past 1:50,000
});

影響: 縮尺の制限がない場合、50 万件のフィーチャを含む区画レイヤーは、州全体のフィーチャをすべてレンダリングしようとします。

3D シーンのパフォーマンス
品質プロファイル
import SceneView from "@arcgis/core/views/SceneView.js";

const view = new SceneView({
  container: "viewDiv",
  map: map,
  qualityProfile: "low", // "low" | "medium" | "high"
});
品質プロファイル効果
"low"テクスチャ解像度を下げ、地形タイルとポリゴン数を減らします
"medium"品質とパフォーマンスの標準的なバランス
"high"テクスチャ解像度を最大にし、地形の詳細を増やします

影響: "high" から "low" に切り替えると、ミドルレンジのハードウェアでフレームレートが 2 倍になることがあります。

ローカル表示モードとグローバル表示モード
// Anti-pattern: using global mode for a focused area
const view = new SceneView({
  container: "viewDiv",
  map: map,
  camera: { position: { longitude: 8.5, latitude: 47.3, z: 500 }, tilt: 70 },
});
// Correct: use local viewing mode for focused scenes
const view = new SceneView({
  container: "viewDiv",
  map: map,
  viewingMode: "local",
  clippingArea: {
    xmin: 8.4,
    ymin: 47.2,
    xmax: 8.6,
    ymax: 47.4,
    spatialReference: { wkid: 4326 },
  },
  camera: { position: { longitude: 8.5, latitude: 47.3, z: 500 }, tilt: 70 },
});

影響: グローバルモードでは地球全体をレンダリングします。ローカルモードでは平面でクリップするため、地形タイルとレンダリングの複雑さが軽減されます。

シャドウのパフォーマンス
// Anti-pattern: enabling shadows for all scenes by default
const view = new SceneView({
  container: "viewDiv",
  map: map,
  environment: { lighting: { directShadowsEnabled: true } },
});
// Correct: enable shadows only when needed
const view = new SceneView({
  container: "viewDiv",
  map: map,
  environment: { lighting: { directShadowsEnabled: false } },
});

// Toggle on demand
function toggleShadows(view, enabled) {
  view.environment.lighting.directShadowsEnabled = enabled;
}

影響: リアルタイムシャドウは、レンダリングコストをおよそ 2 倍にします。

最適化(P2)

レイヤーの遅延読み込み
ナビゲーション時に読み込む
import * as reactiveUtils from "@arcgis/core/core/reactiveUtils.js";

const layerConfigs = [
  { url: url1, extent: region1Extent },
  { url: url2, extent: region2Extent },
];
const loadedLayers = new Set();

reactiveUtils.watch(
  () => view.stationary && view.extent,
  (extent) => {
    if (!extent) return;
    for (const config of layerConfigs) {
      if (!loadedLayers.has(config.url) && extent.intersects(config.extent)) {
        map.add(new FeatureLayer({ url: config.url }));
        loadedLayers.add(config.url);
      }
    }
  },
);
ユーザーの切り替え操作時に読み込む
async function toggleLayer(registry, id, map) {
  const entry = registry.get(id);
  if (!entry) return;

  if (entry.layer) {
    entry.layer.visible = !entry.layer.visible;
  } else {
    entry.layer = new FeatureLayer({ url: entry.url });
    map.add(entry.layer);
    await entry.layer.when();
  }
}
重要度の低いレイヤーの読み込みを遅らせる
// Correct: defer non-critical layers until the browser is idle
map.add(criticalLayer);
await Promise.all([criticalLayer.when(), view.when()]);

function addWhenIdle(layerFactory) {
  if ("requestIdleCallback" in window) {
    requestIdleCallback(() => map.add(layerFactory()));
  } else {
    setTimeout(() => map.add(layerFactory()), 200);
  }
}

addWhenIdle(() => new FeatureLayer({ url: labelsUrl }));
addWhenIdle(() => new FeatureLayer({ url: boundariesUrl }));

影響: requestIdleCallback はブラウザーのアイドル時間に処理をスケジュールし、重要度の低いレイヤーが重要なレンダリングと競合しないようにします。

よくある落とし穴

  1. ビューの変更が頻繁に発生したときにクエリをキャンセルし忘れる:AbortController を使わないと、パンやズームのたびに新しいクエリが開始され、前のクエリが実行中のままになります。

  2. デフォルトで outFields: ["*"] を使う:必要なフィールドだけを必ず指定してください。

  3. returnGeometry: false を設定しない:リスト、テーブル、統計を作成する場合、ジオメトリーは不要なオーバーヘッドです。

  4. SPA で view.destroy() を呼び出さない:前のビューを破棄せずにマップコンポーネントをマウントするたびに、メモリ使用量が増加します。

  5. 起動時にすべてのレイヤーを読み込む:definitionExpression、minScale/maxScale、遅延読み込みを使って、初期の作業対象を減らしてください。

  6. 負荷の高い操作のために view.extent を監視する:更新をまとめるには、view.stationary または promiseUtils.debounce を使ってください。

  7. 本番ビルドでバレルインポートを使う:import Map from "@arcgis/core/Map.js" のようなディープインポートを必ず使ってください。

  8. 必要がないのに 3D でシャドウを有効にする:directShadowsEnabled: true にすると、レンダリングコストが約 2 倍になります。

  9. ローカルシーンでグローバル表示モードを使う:clippingArea とともに viewingMode: "local" を使うと、地球全体のレンダリングを避けられます。

  10. 大規模なポイントデータセットで featureReduction を使わない:2,000 ポイントを超えるデータセットでは、クラスタリングまたはビニングを使ってください。

参考サンプル

  • featurereduction-cluster - クラスタリングの概要
  • featurereduction-binning - フィーチャをビンに集約して密度を視覚化する
  • featurereduction-cluster-query - クラスターにクエリを実行してパフォーマンスを分析する
  • featurelayerview-query - クライアント側で効率的にフィーチャにクエリを実行する
  • layers-featurelayer-large-collection - 大規模なデータセットでもアプリのインタラクティブ性を維持する
  • featurelayer-queryextent - 範囲ベースのリクエストでクエリを最適化する

関連スキル

  • マップ/ビューの初期化パターンについては arcgis-core-maps を参照してください
  • レイヤーの設定とクエリについては arcgis-layers を参照してください
  • reactiveUtils と promiseUtils については arcgis-core-utilities を参照してください
  • ビルドツールの設定については arcgis-starter-app を参照してください

コメント