diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..47bb233 --- /dev/null +++ b/.jules/bolt.md @@ -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. diff --git a/package-lock.json b/package-lock.json index 7aee30e..0f7e096 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,6 +90,7 @@ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", @@ -270,7 +271,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -319,7 +319,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -330,7 +329,6 @@ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -342,7 +340,6 @@ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1521,7 +1518,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/chai": { "version": "5.2.3", @@ -1590,7 +1588,6 @@ "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1602,7 +1599,6 @@ "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" } @@ -1661,7 +1657,6 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -1853,7 +1848,6 @@ "integrity": "sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.6", @@ -1998,7 +1992,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2039,6 +2032,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -2049,6 +2043,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -2297,7 +2292,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/entities": { "version": "6.0.1", @@ -2380,7 +2376,6 @@ "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -3249,6 +3244,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -3471,7 +3467,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3571,6 +3566,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -3595,7 +3591,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3608,7 +3603,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3622,7 +3616,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redent": { "version": "3.0.0", @@ -3913,7 +3908,6 @@ "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.28.0" }, @@ -3946,7 +3940,6 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4011,7 +4004,6 @@ "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -4090,7 +4082,6 @@ "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.1.6", "@vitest/mocker": "4.1.6", diff --git a/src/components/naver-map/useNaverTourismMarkers.ts b/src/components/naver-map/useNaverTourismMarkers.ts index 0239101..3ddb692 100644 --- a/src/components/naver-map/useNaverTourismMarkers.ts +++ b/src/components/naver-map/useNaverTourismMarkers.ts @@ -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(); + const placeById = new Map(); + 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; @@ -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(); + if (previousSelectedTourismPlaceIdRef.current) idsToRefresh.add(previousSelectedTourismPlaceIdRef.current); + if (selectedTourismPlaceId) idsToRefresh.add(selectedTourismPlaceId); for (const placeId of idsToRefresh) { const place = placeById.get(placeId); @@ -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(); + 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;