◆ 256 までは DOM Markers でマーカーが DOM の要素でできてる
  ◆ ズームやパンで要素の座標を調整
◆ 256 からは Tile Markers で Canvas にマーカー画像を描画してタイルにしてる
◆ 数が多くなると Tile Markers のほうが位置調整とかも減るので軽い
  ◆ 表示範囲のタイルだけ作ればいいので画面外のマーカーは表示しなくていい
◆ icon に path を指定したマーカーは 256 を超えても DOM Markers のままになってる
  ◆ DOM Markers は画面外のも更新してるので多くなると重い
  ◆ path じゃなくて画像ファイルにすれば Tile Markers になって軽くなる

マーカーが多いと重い

以前 4, 5 年くらい前に Google Maps でマーカーを大量に表示してたときは重かったのですが それでも 100 とかは余裕で 1000 だったか 10000 だったかになると流石にノート PC だと重くてよく固まるってくらいでした
それから結構たってるので ライブラリの改善面でもマシンスペック面でももっと多くても余裕あるかなと思ってました

それが……なんと 100 件くらいから重くなってきて 300 もあるとすごく重いです

※ブラウザの処理なので具体的な件数は環境によって違います
調べるきっかけになったデスクトップ PC (最近の i5)だと 300 くらいから重いのですが もう買ってから8 年近くになるハイスペックノート PC (i7) だと 1000 あっても普通になめらかでした

マーカーの最適化

仕方ないものだと思ってたのですが変更履歴を見ていたら 3.31 の変更で DOM Markers と Tile Markers という気になることが書かれてました

この名前で調べてもドキュメント中で説明されてないみたいでしたが 名前と実際に Google Maps を使ってるページで devtools で挙動を見た感じではこういうものでした

DOM Markers と Tile Markers

DOM Markers:
マーカーに DOM を使ってる
地図上に絶対座標指定でマーカーを配置
ズームやパン(地図をドラッグして移動)があると地図の位置に合うように DOM の left, top を書き換えて動かしてる
ズーム中に地図サイズが変わってマーカーが瞬間移動じゃなくてなめらかにマーカーも移動するように細かく何度も位置移動が発生してる

Tile Markers:
Canvas に画像として描画してる
新しいタイルをロードしない限りパンではマーカーに関する処理は発生しない
ズームの場合は異なるズームレベルで再描画だけどマーカーの追加や削除や位置の変更をしない限りはキャッシュが有効なはず
はみ出てる部分は部分的に描画して上下左右とくっつけると自然に見える
マウスが乗ってるかは Canvas に描画した部分と JavaScript で座標チェック

一見 Tile Markers のほうが大変そうですが 多くなってきた場合には こっちのほうが軽くなります
なので 256 までは DOM Markers でそれを超えると Tile Markers に変わるという変更になってます

確認方法

実際に DOM Markers かどうかは devtools の要素選択機能 (「select an element」 って書いてるやつ) でマウスをマーカーの上に持っていくとわかります
DOM Markers だと DOM なのでマーカーだけを選択できて クリックすると Element タブでその要素が選択されます
Tile Markers だとそれぞれに DOM はないので地図全体が選択対象になります

使用感

画面上にマーカーがいっぱいある場合は DOM Markers から Tile Markers に切り替わったタイミングでそこまで違いを感じられません
ただ ズームインして画面内の表示数が減ると変わると劇的に変わりました

DOM Markers ではどこにあろうと座標を計算してるようで重いです
それが Tile Markers だと画面にマーカーがほとんど無いとすごく軽くなります
画面に表示するタイミングで地図のタイル画像を読み込むと同時にそのエリアのマーカーを描画してそうです
日本を表示してるときにアメリカやオーストラリアのマーカーは表示する必要ないので困ることはなくてありがたい最適化です

Tile Markers にならない場合がある

画面に映らないと地図上のマーカーが多くても大丈夫なら ズームアウトレベルを制限しておけば マーカーをいっぱいおいても大丈夫かなと思ってたのですが なぜか重いケースがありました
マーカー数は 300 ~ 500 くらいで Tile Marker になってるはずです

現象が画面内にマーカーが少なくてもズームやパンで重いという DOM Markers のときみたいなものだったので devtools で見てみたら DOM Markers になってました
調べてみると DOM Markers になってるものと Tile Markers になってるものが混在していました

使うオプションによっては Canvas 描画はできなくて DOM Markers の必要があるのでしょうか
考えられそうなのは title とかです
ただ調べてみると title があっても Tile Markers になってました
Canvas 上でもマウスの位置でヒットテストして 地図タイル全体の div に title 属性を付けて実現していました

もう少し調べた結果 icon が問題でした
デフォルトアイコンや画像 URL を指定した場合は問題ないのですが icon に path を設定したらダメのようです
path と言っても自分で定義したものではなく predefined の「◯」でも再現します
https://developers.google.com/maps/documentation/javascript/symbols#predefined

この場合のマーカーは DOM ですが内部的には img タグではなくて canvas タグです
他のマーカーとは違いはあるものの Tile Markers 自体が canvas なので技術的に難しいとかはなさそうなんですけどね

対処方法

画像アイコンなら大丈夫なので 実際に作られた DOM Markers の canvas を画像で保存してその画像を icon に設定すれば対処できます
動的に作りたいなら自分で canvas 上に描画して Data URL を指定するとかでしょうか

DEMO

試せるページです
真ん中上のボタンで デフォルトマーカー・画像指定マーカー・path で 「◯」 を指定したマーカーを選んで追加できます
1 回で 100 マーカーをランダムに画面内に配置します

<!doctype html>
<script src="https://maps.googleapis.com/maps/api/js"></script>

<style>
body { margin: 0; width: 100vw; height: 100vh; }
#ctrl { display: flex; }
#marker-num { padding: 3px 8px; background: white; border: 1px solid gray; }
</style>

<script type="module">
const map = new google.maps.Map(document.body, {
center: { lat: 35, lng: 136 },
zoom: 8,
gestureHandling: "greedy",
})

const ctrl = document.createElement("div")
ctrl.id = "ctrl"
ctrl.innerHTML = `
<button name="add1">Add 100 Default</button>
<button name="add2">Add 100 Custom Image</button>
<button name="add3">Add 100</button>
<span id="marker-num">0</span>
`
ctrl.addEventListener("click", eve => {
const name = eve.target.name
const opts = {
add1: {},
add2: {
icon: {
url: "http://maps.google.com/mapfiles/kml/paddle/ylw-stars.png",
scaledSize: new google.maps.Size(24, 24),
}
},
add3: {
icon: {
path: google.maps.SymbolPath.CIRCLE,
scale: 5,
strokeColor: "#8888ff",
strokeWeight: 2,
fillColor: "#FFF",
fillOpacity: 1
}
},
}
const opt = opts[name]
if(!opt) return
makeMarkers(opt)
document.querySelector("#marker-num").textContent = markers.length
})
map.controls[google.maps.ControlPosition.TOP_CENTER].push(ctrl)

const markers = []

function removeMarkers() {
for(const m of markers) {
m.setMap(null)
}
markers.length = 0
}

function makeMarkers(options) {
const b = map.getBounds()
const ne = b.getNorthEast().toJSON()
const sw = b.getSouthWest().toJSON()
const r = (min, max) => Math.random() * (max - min) + min

for(let i=0;i<100;i++){
const pos = { lat: r(sw.lat, ne.lat), lng: r(sw.lng, ne.lng) }
markers.push(new google.maps.Marker({
position: pos,
map,
...options
}))
}
}

Object.assign(window, { map, markers })
</script>