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()));

影响: 如果有 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 会在浏览器空闲期间安排任务,确保非关键图层不会与关键渲染争抢资源。

常见陷阱

  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 会使渲染开销大约增加一倍。

  9. 在局部场景中使用全局查看模式:使用 viewingMode: "local" 和 clippingArea,可避免渲染整个地球。

  10. 大型点数据集未使用 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,了解构建工具配置

评论

支持 Markdown