mcpField
L
large-scale-map-visualization

large-scale-map-visualization

github.com/curiositech开发257229 次安装2 天前更新
代码仓库

关于 large-scale-map-visualization

面向处理大量地理数据的 Web 地图开发者的性能优化技能,重点涵盖 Leaflet.js、空间聚类和视口加载。它还介绍按缩放级别渐进展示、Canvas 渲染及地图交互实践。

主要功能

  1. 01Leaflet.js 地图性能优化
  2. 02Supercluster 空间聚类
  3. 03按视口加载数据
  4. 04按缩放级别渐进展示
  5. 05Canvas 渲染与事件防抖

适用场景

  1. 01优化包含大量标记的地图
  2. 02按当前地图范围加载地点
  3. 03改善实时地图的渲染性能
  4. 04为不同缩放级别调整聚类呈现

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/curiositech/some_claude_skills --skill large-scale-map-visualization

安装后在 Claude Code 中输入/large-scale-map-visualization确认已加载。

附带文件.claude-plugin/plugin.json

详细介绍

大规模地图可视化专家

精通高性能 Web 地图实现,可处理 5,000–100,000+ 个地理数据点。专长包括 Leaflet.js 优化、空间聚类算法、基于视口的加载,以及适用于地图应用的渐进式信息呈现 UX 模式。

激活触发条件

适用于:“地图性能”、“标记太多”、“地图很慢”、“聚类”、“10k 个点”、“标记聚类”、“Leaflet 性能”、“空间可视化”、“地理空间聚类”、“视口加载”、“地图数据优化”、“实时地图”、“Supercluster”、“标记聚类”

**不适用于:**静态地图图片(使用 Mapbox/Google Static)| 3D 可视化(使用 Maplibre GL)| 非地理数据可视化(使用 D3.js/Chart.js)| 标记少于 100 个的简单地图(直接使用 Leaflet 即可)

核心专长

性能架构
┌─────────────────────────────────────────────────────────────┐
│              MAP PERFORMANCE TIERS                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  0-100 markers    → Vanilla Leaflet (no optimization)      │
│  100-1,000        → Basic clustering (react-leaflet-cluster)│
│  1,000-10,000     → Supercluster + viewport loading        │
│  10,000-50,000    → Supercluster + canvas + sampling       │
│  50,000-500,000   → Web Workers + server-side clustering   │
│  500,000+         → MVT tiles + backend pre-aggregation    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
技术栈选择
用例最佳库原因
React + <5k 个点react-leaflet-cluster可直接使用的简单方案,封装了 Leaflet.markercluster
React + 5–50k 个点use-supercluster hook速度快 3–5 倍,感知视口,原生支持 GeoJSON
React + 50k+ 个点supercluster + Web Workers将聚类任务卸载到后台线程
静态站点服务端聚类在构建时预先计算
实时更新Canvas 渲染器 + 采样最大限度减少 DOM 操作

关键技术

1. 使用 Supercluster 进行标记聚类

Supercluster 优于其他方案的原因:

  • **性能:**处理 500k 个点只需 1–2 秒,而 Leaflet.markercluster 需要 8 秒以上
  • **架构:**基于索引的 k-d 树聚类,可在服务端或 Workers 中运行
  • **API:**简单的 GeoJSON 输入/输出
  • **感知视口:**仅对可见点进行聚类

实现模式:

import useSupercluster from "use-supercluster";

export function OptimizedMap({ locations }: { locations: Place[] }) {
  const mapRef = useRef<L.Map | null>(null);
  const [bounds, setBounds] = useState<BBox | null>(null);
  const [zoom, setZoom] = useState(10);

  // Convert to GeoJSON Feature collection
  const points = useMemo(() =>
    locations.map(place => ({
      type: "Feature" as const,
      properties: {
        cluster: false,
        placeId: place.id,
        place
      },
      geometry: {
        type: "Point" as const,
        coordinates: [place.longitude, place.latitude]
      }
    })),
    [locations]
  );

  // Cluster points based on viewport
  const { clusters, supercluster } = useSupercluster({
    points,
    bounds,
    zoom,
    options: {
      radius: 75,        // Cluster radius in pixels
      maxZoom: 16,       // Stop clustering at street level
      minPoints: 2       // Minimum points to form cluster
    }
  });

  // Update viewport on map move
  useEffect(() => {
    if (!mapRef.current) return;

    const handleMove = () => {
      const map = mapRef.current!;
      const b = map.getBounds();
      setBounds([b.getWest(), b.getSouth(), b.getEast(), b.getNorth()]);
      setZoom(map.getZoom());
    };

    mapRef.current.on("moveend", handleMove);
    handleMove(); // Initial load

    return () => mapRef.current?.off("moveend", handleMove);
  }, []);

  return (
    <MapContainer ref={mapRef} preferCanvas={true}>
      {clusters.map(cluster => {
        const [lng, lat] = cluster.geometry.coordinates;
        const { cluster: isCluster, point_count } = cluster.properties;

        if (isCluster) {
          return (
            <Marker
              key={`cluster-${cluster.id}`}
              position={[lat, lng]}
              icon={createClusterIcon(point_count, zoom)}
              eventHandlers={{
                click: () => {
                  const expansionZoom = Math.min(
                    supercluster!.getClusterExpansionZoom(cluster.id),
                    18
                  );
                  mapRef.current?.setView([lat, lng], expansionZoom, {
                    animate: true
                  });
                }
              }}
            />
          );
        }

        return (
          <PlaceMarker
            key={cluster.properties.placeId}
            place={cluster.properties.place}
          />
        );
      })}
    </MapContainer>
  );
}
2. 基于视口加载(Supabase + PostGIS)

数据库函数:

CREATE OR REPLACE FUNCTION find_in_viewport(
  min_lng DOUBLE PRECISION,
  min_lat DOUBLE PRECISION,
  max_lng DOUBLE PRECISION,
  max_lat DOUBLE PRECISION,
  zoom_level INTEGER DEFAULT 11,
  max_results INTEGER DEFAULT 10000
)
RETURNS TABLE (
  id UUID,
  name TEXT,
  latitude DOUBLE PRECISION,
  longitude DOUBLE PRECISION
  /* other fields */
) AS $$
BEGIN
  -- At low zoom levels, sample to reduce density
  IF zoom_level < 9 THEN
    RETURN QUERY
    SELECT
      p.id, p.name,
      ST_Y(p.geog::geometry) as latitude,
      ST_X(p.geog::geometry) as longitude
    FROM places p
    WHERE p.geog && ST_MakeEnvelope(min_lng, min_lat, max_lng, max_lat, 4326)::geography
    AND random() < 0.2  -- Show 20% for performance
    LIMIT max_results / 2;
  ELSE
    -- Full data at higher zoom
    RETURN QUERY
    SELECT
      p.id, p.name,
      ST_Y(p.geog::geometry) as latitude,
      ST_X(p.geog::geometry) as longitude
    FROM places p
    WHERE p.geog && ST_MakeEnvelope(min_lng, min_lat, max_lng, max_lat, 4326)::geography
    LIMIT max_results;
  END IF;
END;
$$ LANGUAGE plpgsql STABLE;

-- Ensure spatial index exists
CREATE INDEX IF NOT EXISTS idx_places_geog ON places USING GIST (geog);

React Query Hook:

import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/lib/supabase";

type BBox = [number, number, number, number]; // [west, south, east, north]

export function usePlacesInViewport(
  bounds: BBox | null,
  zoom: number,
  enabled = true
) {
  return useQuery({
    queryKey: ["places", "viewport", bounds?.join(","), zoom],
    queryFn: async () => {
      if (!bounds) return [];

      const [west, south, east, north] = bounds;

      const { data, error } = await supabase.rpc("find_in_viewport", {
        min_lng: west,
        min_lat: south,
        max_lng: east,
        max_lat: north,
        zoom_level: zoom
      });

      if (error) throw error;
      return data || [];
    },
    enabled: enabled && !!bounds,
    staleTime: 5 * 60 * 1000,    // 5 min (locations rarely change)
    gcTime: 30 * 60 * 1000,       // 30 min in cache
    refetchOnWindowFocus: false
  });
}
3. 渐进式信息呈现策略

根据缩放级别显示适当的详细程度:

const getClusterOptions = (zoom: number) => ({
  radius: zoom < 10 ? 100 : zoom < 14 ? 75 : 50,
  maxZoom: 16,
  minPoints: zoom < 10 ? 5 : 2
});

const getMarkerSize = (zoom: number) =>
  zoom < 12 ? 24 : zoom < 15 ? 32 : 40;

const shouldShowLabel = (zoom: number) => zoom >= 14;
4. 使用 Canvas 渲染以提升性能
import L from "leaflet";

// Enable canvas renderer globally
const canvasRenderer = L.canvas({
  tolerance: 10,      // Hit detection tolerance
  padding: 0.5        // Extra render area (0.5 = 50% of viewport)
});

const mapOptions = {
  preferCanvas: true,
  renderer: canvasRenderer,
  // Disable animations on mobile
  zoomAnimation: !isMobile(),
  fadeAnimation: !isMobile(),
  markerZoomAnimation: !isMobile()
};

**性能提升:**标记数量达到 1,000+ 时,渲染速度快 3–5 倍

5. 高效的聚类图标
import L from "leaflet";

// Use divIcon (faster than custom components)
function createClusterIcon(count: number, zoom: number) {
  const size = getMarkerSize(zoom);

  return L.divIcon({
    html: `
      <div style="
        width: ${size}px;
        height: ${size}px;
        background: linear-gradient(135deg, #d97706, #f59e0b);
        border-radius: 50%;
        border: 3px solid #1a1410;
        display: flex;
        align-items: center;
        justify-content: center;
        color: white;
        font-weight: bold;
        font-size: ${zoom < 12 ? '10px' : '14px'};
        box-shadow: 0 4px 12px rgba(0,0,0,0.4);
      ">
        ${count}
      </div>
    `,
    className: "cluster-icon",
    iconSize: [size, size],
    iconAnchor: [size / 2, size / 2]
  });
}
6. 对地图事件进行防抖处理
import { useDebouncedCallback } from "use-debounce";

const handleMapMove = useDebouncedCallback(() => {
  const bounds = mapRef.current?.getBounds();
  const zoom = mapRef.current?.getZoom();
  if (bounds && zoom) {
    setBounds([
      bounds.getWest(),
      bounds.getSouth(),
      bounds.getEast(),
      bounds.getNorth()
    ]);
    setZoom(zoom);
  }
}, 300); // 300ms debounce

useEffect(() => {
  mapRef.current?.on("moveend", handleMapMove);
  return () => mapRef.current?.off("moveend", handleMapMove);
}, []);

性能基准

基于实际测试和研究(来源见参考资料):

策略1k 个点5k 个点10k 个点移动设备(4G)
不聚类800ms3.5s ❌8s ❌12s ❌
基础聚类400ms1.8s ⚠️4s ⚠️6s ❌
Leaflet.markercluster200ms800ms ⚠️2s ⚠️3s ⚠️
Supercluster + 视口150ms ✅300ms ✅500ms ✅800ms ✅
Supercluster + Canvas100ms ✅200ms ✅350ms ✅500ms ✅

目标性能:

  • 初始加载:<500ms(感知时长)
  • 平移/缩放:响应时间 <200ms
  • 标记点击:<100ms
  • 移动设备:可接受为桌面设备耗时的 2 倍

UX 模式

聚类交互模式
  1. 点击展开(推荐)

    • 点击聚类 → 缩放至展开级别
    • 显示底层点的“蜘蛛”视图
  2. 点击查看列表

    • 点击聚类 → 显示包含所有项目的侧边栏
    • 适用于密集区域(市中心)
  3. 悬停预览

    • 悬停在聚类上 → 显示数量和排名前 3 的项目
    • 适用于探索式 UX
加载状态
{isLoading && (
  <div className="absolute inset-0 bg-leather-900/50 backdrop-blur-sm z-[1000] flex items-center justify-center">
    <div className="text-sand-100">
      Loading {loadedCount} of {totalCount} locations...
    </div>
  </div>
)}
空状态
{!isLoading && clusters.length === 0 && (
  <div className="absolute inset-0 flex items-center justify-center z-[999]">
    <div className="text-center max-w-md p-6">
      <MapPin className="h-12 w-12 text-sand-400 mx-auto mb-4" />
      <h3 className="font-bitter text-xl text-sand-100 mb-2">
        No locations in this area
      </h3>
      <p className="text-sand-400 mb-4">
        Try zooming out or searching a different location.
      </p>
      <button onClick={resetView} className="btn-primary">
        Reset View
      </button>
    </div>
  </div>
)}

常见陷阱

❌ 应避免的反模式
  1. 一次性加载所有数据

    // BAD: Fetches 10k records on mount
    const { data } = useQuery(["all-places"], fetchAllPlaces);
    
  2. 每次地图移动都触发重新渲染

    // BAD: Updates state on every pixel
    map.on("move", () => setBounds(map.getBounds()));
    
  3. 复杂的标记组件

    // BAD: React component per marker
    <Marker icon={<ComplexSVGComponent />} />
    
  4. 不根据缩放级别调整显示内容

    // BAD: Same clustering at all zoom levels
    const clusterOptions = { radius: 80, maxZoom: 20 };
    
✅ 最佳实践
  1. 使用防抖进行基于视口的加载
  2. 使用简单的标记图标(带内联样式的 divIcon)
  3. 渐进式信息呈现(根据缩放级别调整)
  4. 对大型数据集使用 Canvas 渲染
  5. 正确配置 React Query 缓存

实际案例

Zillow 模式
  • 低缩放级别:社区价格聚类
  • 中缩放级别:显示带价格的单个房产
  • 高缩放级别:完整房产卡片
  • 点击:展开聚类或打开详情
Airbnb 模式
  • 服务端:在 10 个缩放级别预先聚类
  • 客户端:视口 API,采用 300ms 防抖
  • 渲染:使用 Canvas 显示价格标签
  • 交互:悬停查看预览,点击查看详情
OpenStreetMap 模式
  • 基于瓦片:预渲染栅格瓦片
  • 矢量瓦片:适用于 100k+ POI
  • 简化:在低缩放级别减少细节
  • 缓存:积极使用 CDN 和浏览器缓存

技术栈兼容性

框架
  • ✅ Next.js 13+(App Router + Server Components)
  • ✅ Next.js Pages Router
  • ✅ Vite + React
  • ✅ Remix
  • ✅ Astro(配合客户端 islands)
数据库
  • ✅ Supabase(PostGIS) - 推荐,内置空间索引
  • ✅ PostgreSQL + PostGIS
  • ⚠️ MongoDB(地理空间查询比 PostGIS 慢)
  • ⚠️ Firebase(地理空间查询支持有限)
地图库
  • ✅ Leaflet.js - 最适合静态瓦片和标记
  • ✅ Mapbox GL JS - 更适合矢量瓦片
  • ✅ Maplibre GL JS - 开源的 Mapbox 替代方案
  • ❌ Google Maps API - 费用高,灵活性较低

迁移检查清单

优化现有的卡顿地图时:

  • 测量当前性能(Chrome DevTools 的 Performance 标签页)
  • 统计数据集中的标记/点总数
  • 检查数据库是否存在空间索引(EXPLAIN ANALYZE)
  • 安装聚类库(npm install use-supercluster)
  • 实现基于视口的数据加载
  • 添加 Canvas 渲染器选项
  • 在移动设备上测试(4G 限速)
  • 添加加载状态
  • 实现渐进式呈现
  • 设置性能监控
  • 记录各缩放级别的行为

依赖项

{
  "dependencies": {
    "leaflet": "^1.9.4",
    "react-leaflet": "^4.2.1",
    "supercluster": "^8.0.1",
    "use-supercluster": "^1.2.0",
    "@tanstack/react-query": "^5.0.0",
    "use-debounce": "^10.0.0"
  }
}

参考资料

研究论文
技术指南
用户体验研究

版本历史

  • 2026-01-09:基于 sobriety.tools 地点地图优化创建初始技能
  • 综合整理了 8 个权威来源的研究成果
  • 已使用 Next.js 15、Leaflet 1.9.4、Supabase PostGIS 进行测试

技能作者:Claude Code(Sonnet 4.5)
领域:地理空间数据可视化、Web 性能
复杂度:高级(需要掌握 PostGIS、React 和空间算法知识)

评论

支持 Markdown