Back to Engineering Log
Feb 17, 20264 min read

120Hz Map Performance: Hooking Native SDKs

Solving micro-stutter on Apple Maps, Google Maps, and Mapbox via delegate swizzling.

M
Mohammad Rashid
View Profile

Capturing high-fidelity session replays of native map views (Apple Maps, Google Maps, Mapbox) on 120Hz "ProMotion" screens is notoriously difficult. The standard approach of repeatedly snapshotting the view hierarchy often leads to micro-stutters and tearingbecause the capture loop fights with the map's own aggressive rendering loop.

At Rejourney, we discovered that simply scheduling captures on a timer wasn't enough. To achieve buttery-smooth 120Hz performance while recording, we had to get deeper:Hooking the native map SDK rendering delegates.

THE CHALLENGE

The 120Hz Conflict

Modern map SDKs drive the GPU hard. On an iPhone 15 Pro, a map sitting idle might be efficient, but the moment a user pans or zooms, the map engine locks the main thread's render server to maintain 120 FPS.

If a session replay SDK tries to force a `drawHierarchy` or `snapshotView` call in the middle of this gesture, two things happen:

  • Dropped Frames (Stutter): The map renderer blocks waiting for the snapshot to complete, causing a visible hitch in the user's scroll.
  • Visual Artifacts: The snapshot might capture a half-rendered buffer state.
THE SOLUTION

Delegate Swizzling & Hooking

Instead of guessing when to capture, we ask the Map SDK itself. We reverse-engineered the delegate lifecycles of the major map providers to identify the exact moments when the map is Idle (safe to capture) vs. Moving (unsafe to capture).

iOS (Swift)

We use method swizzling on the `delegate` property. When we detect a map, we transparently hook into the lifecycle methods to toggle our internal `mapIdle` state.

MKMapViewDelegate
regionWillChangeAnimated -> PAUSE
regionDidChangeAnimated -> CAPTURE
Android (Kotlin)

We use dynamic proxies to intercept the `OnCameraIdleListener`. This allows us to wake up our visual capture engine exactly when the map settles.

GoogleMap.OnCameraIdleListener
onCameraIdle() -> SNAPSHOT NOW

Code Analysis: SpecialCases.swift

Here is the actual logic we use to safely swizzle the Apple Maps delegate without crashing the host app. We verify response to selectors before hooking.

private func _hookAppleMapKit(_ mapView: UIView) {
    guard let delegate = mapView.value(forKey: "delegate") as? NSObject else { return }
    
    // 1. Hook regionWillChange (Movement Start)
    let willChangeSel = NSSelectorFromString("mapView:regionWillChangeAnimated:")
    if let original = class_getInstanceMethod(delegateClass, willChangeSel) {
        let block: @convention(block) (AnyObject, AnyObject, Bool) -> Void = { 
            [weak self] _, _, _ in
            self?.mapIdle = false // <--- PAUSE CAPTURE
            // ... call original implementation ...
        }
        // ... swizzle implementation ...
    }

    // 2. Hook regionDidChange (Movement End)
    let didChangeSel = NSSelectorFromString("mapView:regionDidChangeAnimated:")
    if let original = class_getInstanceMethod(delegateClass, didChangeSel) {
        let block: @convention(block) (AnyObject, AnyObject, Bool) -> Void = { 
            [weak self] _, _, _ in
            self?.mapIdle = true  // <--- RESUME CAPTURE
            VisualCapture.shared.snapshotNow() // <--- CRITICAL: Capture immediately
            // ... call original implementation ...
        }
        // ... swizzle implementation ...
    }
}

The Result: Zero Jitter

By synchronizing our capture loop with the Map SDK's own camera logic, we achieve:

0ms
Main Thread Block
During active map gestures
100%
Frame Integrity
No tearing or half-rendered tiles
Auto
Detection
Works for Mapbox, Google, Apple

Author

M
Mohammad Rashid

Rejourney Engineering Team