arcgis-performance について
arcgis-performance は、ArcGIS Maps SDK for JavaScript の 2D MapView と 3D SceneView を最適化する開発者向けスキルです。準備完了の扱い、読み込み、描画、バンドルサイズ、メモリ管理を扱います。
主な機能
- 01ビューとレイヤーの準備完了処理
- 02レイヤーの並列読み込み
- 03必要なモジュールの個別インポート
- 04フィーチャーのクエリと描画の最適化
- 05ビュー、監視処理、クエリの後処理
活用シーン
- 01マップ初期化の待ち時間削減
- 02大規模フィーチャーデータの効率的な処理
- 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 はブラウザーのアイドル時間に処理をスケジュールし、重要度の低いレイヤーが重要なレンダリングと競合しないようにします。
よくある落とし穴
-
ビューの変更が頻繁に発生したときにクエリをキャンセルし忘れる:AbortController を使わないと、パンやズームのたびに新しいクエリが開始され、前のクエリが実行中のままになります。
-
デフォルトで
outFields: ["*"]を使う:必要なフィールドだけを必ず指定してください。 -
returnGeometry: falseを設定しない:リスト、テーブル、統計を作成する場合、ジオメトリーは不要なオーバーヘッドです。 -
SPA で
view.destroy()を呼び出さない:前のビューを破棄せずにマップコンポーネントをマウントするたびに、メモリ使用量が増加します。 -
起動時にすべてのレイヤーを読み込む:
definitionExpression、minScale/maxScale、遅延読み込みを使って、初期の作業対象を減らしてください。 -
負荷の高い操作のために
view.extentを監視する:更新をまとめるには、view.stationaryまたはpromiseUtils.debounceを使ってください。 -
本番ビルドでバレルインポートを使う:
import Map from "@arcgis/core/Map.js"のようなディープインポートを必ず使ってください。 -
必要がないのに 3D でシャドウを有効にする:
directShadowsEnabled: trueにすると、レンダリングコストが約 2 倍になります。 -
ローカルシーンでグローバル表示モードを使う:
clippingAreaとともにviewingMode: "local"を使うと、地球全体のレンダリングを避けられます。 -
大規模なポイントデータセットで 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を参照してください