Представьте карту с 10 000 точек в мобильном приложении доставки. Без кластеризации интерфейс превращается в кашу, и приложение тормозит. Мы решаем это внедрением кластеризации: на Android с Google Maps Utils, на iOS — через MKClusterAnnotation, на кроссплатформенных проектах — Mapbox или встроенными средствами. Наш опыт показывает: правильно настроенная кластеризация позволяет отображать до 10 000 маркеров без потери плавности (FPS остаётся 60). Мы используем нативные SDK и проверенные библиотеки, гарантируя совместимость с последними версиями SDK.
Почему кластеризация маркеров критична для производительности?
Без кластеризации каждый маркер — отдельный объект на карте. При зуме камера перерисовывает десятки иконок, создавая нагрузку на GPU. Тесты показывают: на Android 500 маркеров без кластеризации снижают FPS до 20, с кластеризацией — стабильные 60. На iOS MKMapView с MKClusterAnnotation практически не теряет производительности даже при 2000 точек. Кластеризация уменьшает нагрузку на GPU в 2–3 раза по сравнению с отображением всех маркеров по отдельности.
| Параметр | Без кластеризации | С кластеризацией |
|---|---|---|
| 1000 маркеров FPS | ~25 | 60 |
| Время загрузки (ms) | ~800 | ~200 |
| UX (оценка) | 2/5 | 5/5 |
Например, в проекте для сети ресторанов с 15 000 точек мы внедрили кластеризацию на Android с кастомным рендерером. После интеграции время отклика карты упало с 1.2 с до 150 мс, а оценки пользователей в магазине выросли на 15%. Это подтверждает, что кластеризация не просто техническая необходимость, а инструмент улучшения UX.
Как кластеризация реализуется на разных платформах?
Как реализовать кластеризацию на Google Maps (Android)
Для Android используем библиотеку maps-utils, предоставляющую готовый ClusterManager. Настройка занимает всего несколько строк:
// build.gradle implementation("com.google.maps.android:android-maps-utils:3.8.2") class ClusteringActivity : AppCompatActivity(), OnMapReadyCallback { private lateinit var clusterManager: ClusterManager<MyClusterItem> override fun onMapReady(googleMap: GoogleMap) { clusterManager = ClusterManager(this, googleMap) googleMap.setOnCameraIdleListener(clusterManager) googleMap.setOnMarkerClickListener(clusterManager) // Кастомный рендерер clusterManager.renderer = CustomClusterRenderer(this, googleMap, clusterManager) // Добавление точек val items = locations.map { MyClusterItem(it.lat, it.lng, it.title) } clusterManager.addItems(items) clusterManager.cluster() } } data class MyClusterItem( private val lat: Double, private val lng: Double, private val title: String ) : ClusterItem { override fun getPosition() = LatLng(lat, lng) override fun getTitle() = title override fun getSnippet() = null override fun getZIndex() = 0f } Кастомный вид кластера
Создаём свой DefaultClusterRenderer, чтобы изменить цвет и форму:
class CustomClusterRenderer( context: Context, map: GoogleMap, clusterManager: ClusterManager<MyClusterItem> ) : DefaultClusterRenderer<MyClusterItem>(context, map, clusterManager) { override fun onBeforeClusterItemRendered(item: MyClusterItem, markerOptions: MarkerOptions) { markerOptions.icon(BitmapDescriptorFactory.fromResource(R.drawable.custom_pin)) } override fun onBeforeClusterRendered( cluster: Cluster<MyClusterItem>, markerOptions: MarkerOptions ) { val count = cluster.size val bitmap = createClusterBitmap(count) markerOptions.icon(BitmapDescriptorFactory.fromBitmap(bitmap)) } private fun createClusterBitmap(count: Int): Bitmap { val size = 80 val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.parseColor("#3498DB") } canvas.drawCircle(size / 2f, size / 2f, size / 2f - 2f, bgPaint) val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE textSize = 28f textAlign = Paint.Align.CENTER } canvas.drawText(count.toString(), size / 2f, size / 2f + 10f, textPaint) return bitmap } } Как работает кластеризация на iOS (MKMapView)
На iOS кластеризация реализуется через MKClusterAnnotation и делегат MKMapViewDelegate. Назначаем каждой аннотации clusteringIdentifier, и карта автоматически группирует их. Согласно документации Apple, это стандартный подход для оптимизации.
import MapKit class MapViewController: UIViewController, MKMapViewDelegate { @IBOutlet weak var mapView: MKMapView! override func viewDidLoad() { super.viewDidLoad() mapView.delegate = self // Добавление точек let annotations = locations.map { location -> MKPointAnnotation in let annotation = MKPointAnnotation() annotation.coordinate = CLLocationCoordinate2D(latitude: location.lat, longitude: location.lng) annotation.title = location.title annotation.clusteringIdentifier = "cluster" // ключ для группировки return annotation } mapView.addAnnotations(annotations) } func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { if let cluster = annotation as? MKClusterAnnotation { let view = MKMarkerAnnotationView(annotation: cluster, reuseIdentifier: "cluster") view.glyphText = "\(cluster.memberAnnotations.count)" view.markerTintColor = UIColor.systemBlue return view } else { let view = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "pin") view.markerTintColor = UIColor.red return view } } } Mapbox: встроенная кластеризация через GeoJSON
Mapbox поддерживает кластеризацию на уровне источника данных — всё на стороне GPU, что даёт максимальную производительность для десятков тысяч точек:
style.addSource( GeoJsonSource.Builder("locations") .featureCollection(featureCollection) .cluster(true) .clusterMaxZoom(14) .clusterRadius(50) .build() ) // Слой кластеров style.addLayer( CircleLayer("clusters", "locations").apply { filter(has("point_count")) circleColor( step(get("point_count"), color(Color.parseColor("#51bbd6")), stop { literal(100); color(Color.parseColor("#f1f075")) }, stop { literal(750); color(Color.parseColor("#f28cb1")) } ) ) circleRadius( step(get("point_count"), literal(20), stop { literal(100); literal(30) }, stop { literal(750); literal(40) } ) ) } ) // Слой с числом внутри кластера style.addLayer( SymbolLayer("cluster-count", "locations").apply { filter(has("point_count")) textField(get("point_count_abbreviated")) textSize(12.0) textColor(Color.WHITE) } ) // Слой одиночных точек style.addLayer( SymbolLayer("unclustered-point", "locations").apply { filter(not(has("point_count"))) iconImage("custom-pin") } ) Тап по кластеру: zoom to bounds
При клике на кластер карта должна приблизиться так, чтобы все его точки вошли в экран. Для Mapbox это реализуется так:
mapView.mapboxMap.addOnMapClickListener { point -> val features = mapView.mapboxMap.queryRenderedFeatures( ScreenCoordinate(point.x, point.y), RenderedQueryOptions(listOf("clusters"), null) ) features.value?.firstOrNull()?.let { feature -> val clusterId = feature.id()?.toLongOrNull() ?: return@addOnMapClickListener true (style.getSource("locations") as? GeoJsonSource)?.getClusterExpansionZoom( Feature.fromGeometry(feature.geometry()!!) ) { result -> result.value?.let { zoom -> mapView.mapboxMap.setCamera( CameraOptions.Builder() .center(feature.geometry() as? Point) .zoom(zoom.toDouble() + 0.5) .build() ) } } } true } Сравнение SDK для кластеризации
| SDK | Платформа | Производительность | Кастомизация | Сложность интеграции |
|---|---|---|---|---|
| Google Maps Utils | Android | Высокая | Полная | Низкая |
| MKClusterAnnotation | iOS | Очень высокая | Средняя | Средняя |
| Mapbox | Кроссплатформа | Максимальная | Полная | Средняя |
Выбор SDK зависит от требований к кросс-платформенности и уровню кастомизации. Для максимальной производительности на больших объёмах лучше всего подходит Mapbox, а для быстрой интеграции в нативные проекты — Google Maps Utils или Apple Maps.
Процесс работы
- Аналитика — определяем количество маркеров, частоту обновления, требуемый UX.
- Проектирование — выбираем SDK (Google Maps, Mapbox, Apple Maps) и стратегию кластеризации.
- Реализация — интегрируем кластеризацию с кастомным видом, анимацией и обработкой тапа.
- Тестирование — проверяем на реальных устройствах (iPhone 14, Pixel 7) с 5000+ маркеров.
- Деплой — публикуем в App Store / Google Play с инструкцией по поддержке.
Сроки и стоимость
Базовая кластеризация занимает 1 день, с кастомизацией и анимацией — до 3 дней. Стоимость рассчитывается индивидуально в зависимости от сложности. Интеграция кластеризации снижает затраты на серверную инфраструктуру и повышает рентабельность приложения. Свяжитесь с нами, чтобы мы оценили ваш проект.
Что входит в работу
- Исходный код с интеграцией кластеризации на выбранной платформе.
- Кастомный вид кластера (цвет, форма, иконки).
- Анимация раскрытия/сжатия кластера.
- Обработка тапа по кластеру (zoom to bounds).
- Юнит-тесты и UI-тесты на производительность.
- Документация по конфигурации и поддержке.
- Гарантия совместимости с последними версиями SDK.
Типичные ошибки при реализации кластеризации
- Слишком маленький радиус кластеризации — кластеры не формируются, смысла нет. Рекомендуем начинать с 50–60 px.
- Большой minZoom при малом числе точек — кластеризация не работает, пользователь видит исходный хаос.
- Отсутствие кастомного рендерера — дефолтные стили не вписываются в дизайн приложения.
- Игнорирование обработки тапа — без zoom to bounds UX ломается: пользователь не может увидеть отдельные точки.
- Нет тестов на 5000+ маркеров — на реальных данных подтормаживания незаметны в тестовой среде.
Наши инженеры — сертифицированные разработчики Apple и Google, имеющие опыт внедрения кластеризации в 12+ проектах. Закажите консультацию, чтобы получить реализацию кластеризации для вашего приложения.







