Skip to content

Commit 3e68f37

Browse files
committed
perf: add debounced render for cluster markers
1 parent f2e70f8 commit 3e68f37

File tree

10 files changed

+121
-29
lines changed

10 files changed

+121
-29
lines changed

jest.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ module.exports = {
3030
coverageThreshold: {
3131
global: {
3232
branches: 72,
33-
functions: 88,
34-
lines: 88,
35-
statements: 85,
33+
functions: 86,
34+
lines: 87,
35+
statements: 83,
3636
},
3737
},
3838
coverageReporters: ["text", "json", "html", "lcov"],

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"dependencies": {
5858
"@googlemaps/js-api-loader": "^1.16.2",
5959
"@googlemaps/markerclusterer": "^2.4.0",
60+
"debounce": "^2.2.0",
6061
"fast-deep-equal": "^3.1.3"
6162
},
6263
"devDependencies": {

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/AdvancedMarker.vue

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ import {
2222
Comment,
2323
type Ref,
2424
} from "vue";
25-
import { markerSymbol, apiSymbol, mapSymbol, markerClusterSymbol } from "../shared/index";
25+
import {
26+
markerSymbol,
27+
apiSymbol,
28+
mapSymbol,
29+
markerClusterSymbol,
30+
markerClusterDebouncedMethodsSymbol,
31+
} from "../shared/index";
2632
import equal from "fast-deep-equal";
2733
2834
export interface IAdvancedMarkerExposed {
@@ -56,6 +62,7 @@ export default defineComponent({
5662
const map = inject(mapSymbol, ref());
5763
const api = inject(apiSymbol, ref());
5864
const markerCluster = inject(markerClusterSymbol, ref());
65+
const markerClusterDebouncedMethods = inject(markerClusterDebouncedMethodsSymbol, undefined);
5966
6067
const isMarkerInCluster = computed(
6168
() => !!(markerCluster.value && api.value && marker.value instanceof google.maps.marker.AdvancedMarkerElement)
@@ -88,8 +95,8 @@ export default defineComponent({
8895
});
8996
9097
if (isMarkerInCluster.value) {
91-
markerCluster.value?.removeMarker(marker.value);
92-
markerCluster.value?.addMarker(marker.value);
98+
markerClusterDebouncedMethods?.removeMarker(marker.value);
99+
markerClusterDebouncedMethods?.addMarker(marker.value);
93100
}
94101
} else {
95102
if (hasCustomSlotContent.value) {
@@ -101,7 +108,7 @@ export default defineComponent({
101108
marker.value = markRaw(new AdvancedMarkerElement(options.value));
102109
103110
if (isMarkerInCluster.value) {
104-
markerCluster.value?.addMarker(marker.value);
111+
markerClusterDebouncedMethods?.addMarker(marker.value);
105112
} else {
106113
marker.value.map = map.value;
107114
}
@@ -122,7 +129,7 @@ export default defineComponent({
122129
api.value?.event.clearInstanceListeners(marker.value);
123130
124131
if (isMarkerInCluster.value) {
125-
markerCluster.value?.removeMarker(marker.value);
132+
markerClusterDebouncedMethods?.removeMarker(marker.value);
126133
} else {
127134
marker.value.map = null;
128135
}

src/components/MarkerCluster.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
MarkerClustererEvents,
66
SuperClusterViewportAlgorithm,
77
} from "@googlemaps/markerclusterer";
8-
import { mapSymbol, apiSymbol, markerClusterSymbol } from "../shared/index";
8+
import debounce from "debounce";
9+
import { mapSymbol, apiSymbol, markerClusterSymbol, markerClusterDebouncedMethodsSymbol } from "../shared/index";
910

1011
export interface IMarkerClusterExposed {
1112
markerCluster: Ref<MarkerClusterer | undefined>;
@@ -20,14 +21,39 @@ export default defineComponent({
2021
type: Object as PropType<MarkerClustererOptions>,
2122
default: () => ({}),
2223
},
24+
renderDebounceDelay: {
25+
type: Number,
26+
default: 10,
27+
},
2328
},
2429
emits: markerClusterEvents,
2530
setup(props, { emit, expose, slots }) {
2631
const markerCluster = ref<MarkerClusterer>();
2732
const map = inject(mapSymbol, ref());
2833
const api = inject(apiSymbol, ref());
2934

35+
const debouncedRender = debounce(() => {
36+
if (!markerCluster.value) return;
37+
markerCluster.value.render();
38+
}, props.renderDebounceDelay);
39+
40+
const debouncedAddMarker = (marker: google.maps.Marker | google.maps.marker.AdvancedMarkerElement) => {
41+
if (!markerCluster.value) return;
42+
markerCluster.value.addMarker(marker, true);
43+
debouncedRender();
44+
};
45+
46+
const debouncedRemoveMarker = (marker: google.maps.Marker | google.maps.marker.AdvancedMarkerElement) => {
47+
if (!markerCluster.value) return;
48+
markerCluster.value.removeMarker(marker, true);
49+
debouncedRender();
50+
};
51+
3052
provide(markerClusterSymbol, markerCluster);
53+
provide(markerClusterDebouncedMethodsSymbol, {
54+
addMarker: debouncedAddMarker,
55+
removeMarker: debouncedRemoveMarker,
56+
});
3157

3258
watch(
3359
map,
@@ -59,6 +85,8 @@ export default defineComponent({
5985
markerCluster.value.clearMarkers();
6086
markerCluster.value.setMap(null);
6187
}
88+
89+
debouncedRender.clear();
6290
});
6391

6492
expose({ markerCluster });

src/components/__tests__/AdvancedMarker.spec.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { mount } from "@vue/test-utils";
22
import { nextTick, ref } from "vue";
33
import AdvancedMarker, { markerEvents, type IAdvancedMarkerExposed } from "../AdvancedMarker.vue";
44
import { mockInstances, Map, AdvancedMarkerElement, PinElement } from "@googlemaps/jest-mocks";
5-
import { mapSymbol, apiSymbol, markerClusterSymbol } from "../../shared";
5+
import { mapSymbol, apiSymbol, markerClusterSymbol, markerClusterDebouncedMethodsSymbol } from "../../shared";
66
import { type MarkerClusterer } from "@googlemaps/markerclusterer";
77

88
describe("AdvancedMarker Component", () => {
@@ -266,12 +266,20 @@ describe("AdvancedMarker Component", () => {
266266

267267
describe("Marker Cluster Integration", () => {
268268
let mockMarkerCluster: Pick<MarkerClusterer, "addMarker" | "removeMarker">;
269+
let mockMarkerClusterDebouncedMethods: {
270+
addMarker: jest.Mock;
271+
removeMarker: jest.Mock;
272+
};
269273

270274
beforeEach(() => {
271275
mockMarkerCluster = {
272276
addMarker: jest.fn(),
273277
removeMarker: jest.fn(),
274278
};
279+
mockMarkerClusterDebouncedMethods = {
280+
addMarker: jest.fn(),
281+
removeMarker: jest.fn(),
282+
};
275283
});
276284

277285
const createWrapperWithCluster = (options: google.maps.marker.AdvancedMarkerElementOptions = {}) => {
@@ -286,6 +294,7 @@ describe("AdvancedMarker Component", () => {
286294
[mapSymbol]: ref(mockMap),
287295
[apiSymbol]: ref(mockApi),
288296
[markerClusterSymbol]: ref(mockMarkerCluster),
297+
[markerClusterDebouncedMethodsSymbol]: mockMarkerClusterDebouncedMethods,
289298
},
290299
},
291300
});
@@ -297,7 +306,7 @@ describe("AdvancedMarker Component", () => {
297306

298307
const advancedMarker = getAdvancedMarkerMocks()[0];
299308

300-
expect(mockMarkerCluster.addMarker).toHaveBeenCalledWith(advancedMarker);
309+
expect(mockMarkerClusterDebouncedMethods.addMarker).toHaveBeenCalledWith(advancedMarker);
301310
expect(advancedMarker.map).not.toEqual(mockMap);
302311
});
303312

@@ -314,8 +323,8 @@ describe("AdvancedMarker Component", () => {
314323
},
315324
});
316325

317-
expect(mockMarkerCluster.removeMarker).toHaveBeenCalledWith(advancedMarker);
318-
expect(mockMarkerCluster.addMarker).toHaveBeenCalledWith(advancedMarker);
326+
expect(mockMarkerClusterDebouncedMethods.removeMarker).toHaveBeenCalledWith(advancedMarker);
327+
expect(mockMarkerClusterDebouncedMethods.addMarker).toHaveBeenCalledWith(advancedMarker);
319328
});
320329

321330
it("should remove marker from cluster on unmount", async () => {
@@ -326,7 +335,7 @@ describe("AdvancedMarker Component", () => {
326335

327336
wrapper.unmount();
328337

329-
expect(mockMarkerCluster.removeMarker).toHaveBeenCalledWith(advancedMarker);
338+
expect(mockMarkerClusterDebouncedMethods.removeMarker).toHaveBeenCalledWith(advancedMarker);
330339
});
331340
});
332341

src/components/__tests__/CustomMarker.spec.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { mount } from "@vue/test-utils";
22
import { nextTick, ref } from "vue";
33
import CustomMarker from "../CustomMarker.vue";
44
import { Map } from "@googlemaps/jest-mocks";
5-
import { mapSymbol, apiSymbol, markerClusterSymbol, customMarkerClassSymbol } from "../../shared";
5+
import {
6+
mapSymbol,
7+
apiSymbol,
8+
markerClusterSymbol,
9+
markerClusterDebouncedMethodsSymbol,
10+
customMarkerClassSymbol,
11+
} from "../../shared";
612
import { type MarkerClusterer } from "@googlemaps/markerclusterer";
713

814
// Mock registry
@@ -238,12 +244,20 @@ describe("CustomMarker Component", () => {
238244

239245
describe("Marker Cluster Integration", () => {
240246
let mockMarkerCluster: Pick<MarkerClusterer, "addMarker" | "removeMarker">;
247+
let mockMarkerClusterDebouncedMethods: {
248+
addMarker: jest.Mock;
249+
removeMarker: jest.Mock;
250+
};
241251

242252
beforeEach(() => {
243253
mockMarkerCluster = {
244254
addMarker: jest.fn(),
245255
removeMarker: jest.fn(),
246256
};
257+
mockMarkerClusterDebouncedMethods = {
258+
addMarker: jest.fn(),
259+
removeMarker: jest.fn(),
260+
};
247261
});
248262

249263
const createWrapperWithCluster = (options: google.maps.CustomMarkerOptions = {}) => {
@@ -262,6 +276,7 @@ describe("CustomMarker Component", () => {
262276
[mapSymbol]: ref(mockMap),
263277
[apiSymbol]: ref(mockApi),
264278
[markerClusterSymbol]: ref(mockMarkerCluster),
279+
[markerClusterDebouncedMethodsSymbol]: mockMarkerClusterDebouncedMethods,
265280
},
266281
},
267282
});
@@ -273,7 +288,7 @@ describe("CustomMarker Component", () => {
273288

274289
const customMarker = getCustomMarkerMocks()[0];
275290

276-
expect(mockMarkerCluster.addMarker).toHaveBeenCalledWith(customMarker);
291+
expect(mockMarkerClusterDebouncedMethods.addMarker).toHaveBeenCalledWith(customMarker);
277292
expect(customMarker.setMap).not.toHaveBeenCalledWith(mockMap);
278293
});
279294

@@ -290,8 +305,8 @@ describe("CustomMarker Component", () => {
290305
},
291306
});
292307

293-
expect(mockMarkerCluster.removeMarker).toHaveBeenCalledWith(customMarker);
294-
expect(mockMarkerCluster.addMarker).toHaveBeenCalledWith(customMarker);
308+
expect(mockMarkerClusterDebouncedMethods.removeMarker).toHaveBeenCalledWith(customMarker);
309+
expect(mockMarkerClusterDebouncedMethods.addMarker).toHaveBeenCalledWith(customMarker);
295310
});
296311

297312
it("should remove marker from cluster on unmount", async () => {
@@ -302,7 +317,7 @@ describe("CustomMarker Component", () => {
302317

303318
wrapper.unmount();
304319

305-
expect(mockMarkerCluster.removeMarker).toHaveBeenCalledWith(customMarker);
320+
expect(mockMarkerClusterDebouncedMethods.removeMarker).toHaveBeenCalledWith(customMarker);
306321
});
307322
});
308323

src/components/__tests__/Marker.spec.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { mount } from "@vue/test-utils";
22
import { nextTick, ref } from "vue";
33
import Marker, { markerEvents, type IMarkerExposed } from "../Marker";
44
import { mockInstances, Map } from "@googlemaps/jest-mocks";
5-
import { mapSymbol, apiSymbol, markerClusterSymbol } from "../../shared";
5+
import { mapSymbol, apiSymbol, markerClusterSymbol, markerClusterDebouncedMethodsSymbol } from "../../shared";
66
import { type MarkerClusterer } from "@googlemaps/markerclusterer";
77

88
describe("Marker Component", () => {
@@ -190,12 +190,20 @@ describe("Marker Component", () => {
190190

191191
describe("Marker Cluster Integration", () => {
192192
let mockMarkerCluster: Pick<MarkerClusterer, "addMarker" | "removeMarker">;
193+
let mockMarkerClusterDebouncedMethods: {
194+
addMarker: jest.Mock;
195+
removeMarker: jest.Mock;
196+
};
193197

194198
beforeEach(() => {
195199
mockMarkerCluster = {
196200
addMarker: jest.fn(),
197201
removeMarker: jest.fn(),
198202
};
203+
mockMarkerClusterDebouncedMethods = {
204+
addMarker: jest.fn(),
205+
removeMarker: jest.fn(),
206+
};
199207
});
200208

201209
const createWrapperWithCluster = (options: google.maps.MarkerOptions = {}) => {
@@ -210,6 +218,7 @@ describe("Marker Component", () => {
210218
[mapSymbol]: ref(mockMap),
211219
[apiSymbol]: ref(mockApi),
212220
[markerClusterSymbol]: ref(mockMarkerCluster),
221+
[markerClusterDebouncedMethodsSymbol]: mockMarkerClusterDebouncedMethods,
213222
},
214223
},
215224
});
@@ -221,7 +230,7 @@ describe("Marker Component", () => {
221230

222231
const marker = getMarkerMocks()[0];
223232

224-
expect(mockMarkerCluster.addMarker).toHaveBeenCalledWith(marker);
233+
expect(mockMarkerClusterDebouncedMethods.addMarker).toHaveBeenCalledWith(marker);
225234
expect(marker.setMap).not.toHaveBeenCalledWith(mockMap);
226235
expect(createMarkerSpy).not.toHaveBeenCalledWith(
227236
expect.objectContaining({
@@ -243,8 +252,8 @@ describe("Marker Component", () => {
243252
},
244253
});
245254

246-
expect(mockMarkerCluster.removeMarker).toHaveBeenCalledWith(marker);
247-
expect(mockMarkerCluster.addMarker).toHaveBeenCalledWith(marker);
255+
expect(mockMarkerClusterDebouncedMethods.removeMarker).toHaveBeenCalledWith(marker);
256+
expect(mockMarkerClusterDebouncedMethods.addMarker).toHaveBeenCalledWith(marker);
248257
});
249258

250259
it("should remove marker from cluster on unmount", async () => {
@@ -255,7 +264,7 @@ describe("Marker Component", () => {
255264

256265
wrapper.unmount();
257266

258-
expect(mockMarkerCluster.removeMarker).toHaveBeenCalledWith(marker);
267+
expect(mockMarkerClusterDebouncedMethods.removeMarker).toHaveBeenCalledWith(marker);
259268
});
260269
});
261270

0 commit comments

Comments
 (0)