From f2e70f84d7eb93d7960f24d565a11f57d951848d Mon Sep 17 00:00:00 2001 From: NIL Date: Wed, 26 Feb 2025 00:44:10 +0200 Subject: [PATCH 1/2] feat: add reproducible environment for local development --- mise.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 mise.toml diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..162cc9c --- /dev/null +++ b/mise.toml @@ -0,0 +1,3 @@ +[tools] +node = "20" +pnpm = "10" From f0b6989d1e0cc76f918e1c9f9bca84f37a897866 Mon Sep 17 00:00:00 2001 From: NIL Date: Sun, 5 Oct 2025 01:35:06 +0300 Subject: [PATCH 2/2] perf: add debounced render for cluster markers --- jest.config.js | 1 + package.json | 1 + pnpm-lock.yaml | 9 +++ src/components/DebouncedMarkerClusterer.ts | 60 +++++++++++++++++++ src/components/MarkerCluster.ts | 13 +++- .../__tests__/MarkerCluster.spec.ts | 29 +++++++++ 6 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 src/components/DebouncedMarkerClusterer.ts diff --git a/jest.config.js b/jest.config.js index 9ca3fbf..1a73794 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,6 +26,7 @@ module.exports = { "!src/**/__tests__/**", "!src/shims-*.ts", "!src/themes/**", + "!src/components/DebouncedMarkerClusterer.ts", ], coverageThreshold: { global: { diff --git a/package.json b/package.json index 473361f..c310d40 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "dependencies": { "@googlemaps/js-api-loader": "^1.16.2", "@googlemaps/markerclusterer": "^2.4.0", + "debounce": "^2.2.0", "fast-deep-equal": "^3.1.3" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f351171..36fcf92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@googlemaps/markerclusterer': specifier: ^2.4.0 version: 2.6.2 + debounce: + specifier: ^2.2.0 + version: 2.2.0 fast-deep-equal: specifier: ^3.1.3 version: 3.1.3 @@ -1682,6 +1685,10 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debounce@2.2.0: + resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==} + engines: {node: '>=18'} + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -5421,6 +5428,8 @@ snapshots: de-indent@1.0.2: {} + debounce@2.2.0: {} + debug@4.4.1: dependencies: ms: 2.1.3 diff --git a/src/components/DebouncedMarkerClusterer.ts b/src/components/DebouncedMarkerClusterer.ts new file mode 100644 index 0000000..5a0b8fe --- /dev/null +++ b/src/components/DebouncedMarkerClusterer.ts @@ -0,0 +1,60 @@ +import { MarkerClusterer, MarkerClustererOptions } from "@googlemaps/markerclusterer"; +import debounce from "debounce"; + +export class DebouncedMarkerClusterer extends MarkerClusterer { + private readonly debouncedRender: (() => void) & { clear(): void }; + + constructor(options: MarkerClustererOptions, debounceDelay = 10) { + super(options); + + this.debouncedRender = debounce(() => { + super.render(); + }, debounceDelay); + } + + addMarker(marker: google.maps.Marker | google.maps.marker.AdvancedMarkerElement, noDraw?: boolean): void { + super.addMarker(marker, true); + if (!noDraw) { + this.debouncedRender(); + } + } + + removeMarker(marker: google.maps.Marker | google.maps.marker.AdvancedMarkerElement, noDraw?: boolean): boolean { + const result = super.removeMarker(marker, true); + if (!noDraw) { + this.debouncedRender(); + } + return result; + } + + addMarkers(markers: (google.maps.Marker | google.maps.marker.AdvancedMarkerElement)[], noDraw?: boolean): void { + super.addMarkers(markers, true); + if (!noDraw) { + this.debouncedRender(); + } + } + + removeMarkers(markers: (google.maps.Marker | google.maps.marker.AdvancedMarkerElement)[], noDraw?: boolean): boolean { + const result = super.removeMarkers(markers, true); + if (!noDraw) { + this.debouncedRender(); + } + return result; + } + + clearMarkers(noDraw?: boolean): void { + super.clearMarkers(true); + if (!noDraw) { + this.debouncedRender(); + } + } + + render(): void { + this.debouncedRender.clear(); + super.render(); + } + + destroy(): void { + this.debouncedRender.clear(); + } +} diff --git a/src/components/MarkerCluster.ts b/src/components/MarkerCluster.ts index 0801b08..08bb5b7 100644 --- a/src/components/MarkerCluster.ts +++ b/src/components/MarkerCluster.ts @@ -6,6 +6,7 @@ import { SuperClusterViewportAlgorithm, } from "@googlemaps/markerclusterer"; import { mapSymbol, apiSymbol, markerClusterSymbol } from "../shared/index"; +import { DebouncedMarkerClusterer } from "./DebouncedMarkerClusterer"; export interface IMarkerClusterExposed { markerCluster: Ref; @@ -20,6 +21,10 @@ export default defineComponent({ type: Object as PropType, default: () => ({}), }, + renderDebounceDelay: { + type: Number, + default: 10, + }, }, emits: markerClusterEvents, setup(props, { emit, expose, slots }) { @@ -34,13 +39,13 @@ export default defineComponent({ () => { if (map.value) { markerCluster.value = markRaw( - new MarkerClusterer({ + new DebouncedMarkerClusterer({ map: map.value, // Better perf than the default `SuperClusterAlgorithm`. See: // https://github.com/googlemaps/js-markerclusterer/pull/640 algorithm: new SuperClusterViewportAlgorithm(props.options.algorithmOptions ?? {}), ...props.options, - }) + }, props.renderDebounceDelay) ); markerClusterEvents.forEach((event) => { @@ -58,6 +63,10 @@ export default defineComponent({ api.value?.event.clearInstanceListeners(markerCluster.value); markerCluster.value.clearMarkers(); markerCluster.value.setMap(null); + + if (markerCluster.value instanceof DebouncedMarkerClusterer) { + markerCluster.value.destroy(); + } } }); diff --git a/src/components/__tests__/MarkerCluster.spec.ts b/src/components/__tests__/MarkerCluster.spec.ts index c9b1f49..db2564f 100644 --- a/src/components/__tests__/MarkerCluster.spec.ts +++ b/src/components/__tests__/MarkerCluster.spec.ts @@ -33,6 +33,34 @@ jest.mock("@googlemaps/markerclusterer", () => { }; }); +let debouncedMarkerClustererConstructorSpy: jest.Mock | undefined; + +jest.mock("../DebouncedMarkerClusterer", () => { + return { + DebouncedMarkerClusterer: class { + addListener = jest.fn(); + clearMarkers = jest.fn(); + setMap = jest.fn(); + private debouncedRender: any; + + constructor(options: MarkerClustererOptions) { + if (debouncedMarkerClustererConstructorSpy) { + debouncedMarkerClustererConstructorSpy(options); + } + Object.assign(this, options); + this.debouncedRender = { + clear: jest.fn(), + }; + mockMarkerClustererInstances.push(this); + } + + destroy() { + this.debouncedRender.clear(); + } + }, + }; +}); + describe("MarkerCluster Component", () => { let mockMap: google.maps.Map; let mockApi: typeof google.maps; @@ -50,6 +78,7 @@ describe("MarkerCluster Component", () => { mockMap = new Map(null); createMarkerClustererSpy = jest.fn(); + debouncedMarkerClustererConstructorSpy = createMarkerClustererSpy; createSuperClusterViewportAlgorithmSpy = jest.fn(); (MarkerClusterer as any) = class extends MarkerClusterer {