关于 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()));
影响: 如果有 4 个图层,每个加载耗时 500ms,顺序加载需要约 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 可防止某个图层加载失败而阻塞其他所有图层。
包体积
核心 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"],
});
影响: 加载 100,000 个要素再筛选至 500 个,会浪费带宽、内存和 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 就可能占用 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 严格模式会将组件挂载和卸载两次。如果不进行清理,就会创建两个视图,但只有一个可见。
注意: 地图组件(
<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。这样可将每次交互触发的查询次数从数百次减少到一次。
对查询进行防抖
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
});
影响: 如果不设置比例尺限制,包含 500,000 个要素的宗地图层会尝试在州级范围内渲染所有要素。
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",可使中端硬件的帧率翻倍。 |
本地与全球查看模式
// 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;
}
影响: 实时阴影会使渲染成本大约翻倍。
优化(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会使渲染开销大约增加一倍。 -
在局部场景中使用全局查看模式:使用
viewingMode: "local"和clippingArea,可避免渲染整个地球。 -
大型点数据集未使用 featureReduction:超过 2,000 个点的数据集应使用聚类或分箱。
参考示例
featurereduction-cluster- 聚类简介featurereduction-binning- 将要素汇总到分箱中,以可视化密度featurereduction-cluster-query- 查询聚类结果以进行性能分析featurelayerview-query- 高效的客户端要素查询layers-featurelayer-large-collection- 使用大型数据集保持应用的交互性featurelayer-queryextent- 使用基于范围的请求优化查询
相关技能
- 查看
arcgis-core-maps,了解地图/视图初始化模式 - 查看
arcgis-layers,了解图层配置和查询 - 查看
arcgis-core-utilities,了解 reactiveUtils 和 promiseUtils - 查看
arcgis-starter-app,了解构建工具配置