Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-07-21 - [Avoid chained maps and spreads in tight React rendering/effects]
**Learning:** When building arrays, sets, maps, or string signatures for batch operations in tight update loops (like Naver Maps viewport marker materialization), chained `.map().filter()`, `.map().join()`, and spread operators (`[...arr.map(...)]`) create significant garbage collection pressure by allocating intermediate arrays.
**Action:** Replace these declarative method chains with explicit `for...of` loops, pushing items to arrays or setting them directly in Sets/Maps to achieve O(1) memory complexity.
31 changes: 11 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 39 additions & 23 deletions src/components/naver-map/useNaverTourismMarkers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,15 @@ export function useNaverTourismMarkers({
selectedTourismPlaceId,
tourismPlaces,
});
const nextIds = new Set(visiblePlaces.map((place) => place.id));
const visibleSignature = visiblePlaces.map((place) => `${place.id}:${place.latitude}:${place.longitude}`).join('|');
const nextIds = new Set<string>();
const placeById = new Map<string, typeof visiblePlaces[number]>();
const visibleSignatureArray: string[] = [];
for (const place of visiblePlaces) {
nextIds.add(place.id);
placeById.set(place.id, place);
visibleSignatureArray.push(`${place.id}:${place.latitude}:${place.longitude}`);
}
const visibleSignature = visibleSignatureArray.join('|');
const markerAnchor = new mapsApi.Point(NaverMarkerConfig.anchor.default.x, NaverMarkerConfig.anchor.default.y);
let cancelled = false;

Expand All @@ -69,12 +76,10 @@ export function useNaverTourismMarkers({
marker.setZIndex(zIndex);
};

const placeById = new Map(visiblePlaces.map((place) => [place.id, place]));
if (previousVisibleSignatureRef.current === visibleSignature && !markerBatchPendingRef.current) {
const idsToRefresh = new Set([
previousSelectedTourismPlaceIdRef.current,
selectedTourismPlaceId,
].filter((placeId): placeId is string => Boolean(placeId)));
const idsToRefresh = new Set<string>();
if (previousSelectedTourismPlaceIdRef.current) idsToRefresh.add(previousSelectedTourismPlaceIdRef.current);
if (selectedTourismPlaceId) idsToRefresh.add(selectedTourismPlaceId);

for (const placeId of idsToRefresh) {
const place = placeById.get(placeId);
Expand Down Expand Up @@ -115,27 +120,38 @@ export function useNaverTourismMarkers({
tourismMarkersRef.current.set(place.id, marker);
};

const stalePlaceIds = Array.from(tourismMarkersRef.current.keys()).filter((placeId) => !nextIds.has(placeId));
const placesToCreate = visiblePlaces.filter((place) => !tourismMarkersRef.current.has(place.id));
const idsToRefresh = new Set([
previousSelectedTourismPlaceIdRef.current,
selectedTourismPlaceId,
].filter((placeId): placeId is string => Boolean(placeId)));
const operations = [
...stalePlaceIds.map((placeId) => () => {
const marker = tourismMarkersRef.current.get(placeId);
marker?.setMap(null);
tourismMarkersRef.current.delete(placeId);
}),
...placesToCreate.map((place) => () => createMarker(place)),
...Array.from(idsToRefresh).map((placeId) => () => {
const operations: (() => void)[] = [];

for (const placeId of tourismMarkersRef.current.keys()) {
if (!nextIds.has(placeId)) {
operations.push(() => {
const marker = tourismMarkersRef.current.get(placeId);
marker?.setMap(null);
tourismMarkersRef.current.delete(placeId);
});
}
}

for (const place of visiblePlaces) {
if (!tourismMarkersRef.current.has(place.id)) {
operations.push(() => createMarker(place));
}
}

const idsToRefresh = new Set<string>();
if (previousSelectedTourismPlaceIdRef.current) idsToRefresh.add(previousSelectedTourismPlaceIdRef.current);
if (selectedTourismPlaceId) idsToRefresh.add(selectedTourismPlaceId);

for (const placeId of idsToRefresh) {
operations.push(() => {
const place = placeById.get(placeId);
const marker = tourismMarkersRef.current.get(placeId);
if (place && marker) {
updateMarkerVisual(place, marker);
}
}),
];
});
}

let nextOperationIndex = 0;
markerBatchPendingRef.current = operations.length > 0;

Expand Down
Loading