7가지 숨겨진 JavaScript 성능 병목 현상

느린 웹 앱이 발생하는 원인은 알고리즘이 나쁘기 때문인 경우가 드뭅니다. 코드가 브라우저와 통신하는 방식 때문에 발생합니다.

저는 300개 이상의 프로덕션 앱을 프로파일링했습니다. 그 결과 성능 문제의 73%가 다음 7가지 원인에서 발생한다는 것을 발견했습니다.

  1. 레이아웃 스래싱 (Layout thrashing) 레이아웃 속성을 읽은 직후에 바로 DOM에 값을 쓰는 경우 발생합니다. 이로 인해 브라우저가 레이아웃을 여러 번 다시 계산해야 합니다. • 영향: 렌더링 속도 40-60% 저하. • 해결책: 먼저 모든 읽기 작업을 배치(batch)로 처리하세요. 그 다음 requestAnimationFrame을 사용하여 모든 쓰기 작업을 배치로 처리하세요.

  2. 해제되지 않은 이벤트 리스너 (Unbounded event listeners) 리스너를 추가한 후 제거하지 않으면 메모리 누수가 발생합니다. 이는 싱글 페이지 애플리케이션(SPA)에서 주요한 문제입니다. • 영향: 시간당 15-30%의 메모리 증가. • 해결책: 컴포넌트가 언마운트(unmount)될 때 AbortController를 사용하여 리스너를 정리하세요.

  3. 루프 내의 동기적 DOM 읽기 (Synchronous DOM reads in loops) DOM에 값을 쓰는 루프 내부에서 offsetWidth와 같은 속성을 읽으면 지속적인 리플로우(reflow)가 발생합니다. • 영향: 20-40%의 프레임 드랍. • 해결책: 루프가 시작되기 전에 레이아웃 값을 변수에 캐싱해 두세요.

  4. requestAnimationFrame 배칭 누락 (Missing requestAnimationFrame batching) 스크롤이나 리사이즈 이벤트 발생 시 직접적으로 DOM을 변경하면 너무 자주 실행됩니다. • 영향: 스크롤 중 10-25%의 버벅임(jank) 발생. • 해결책: 페인트 사이클(paint cycle)과 동기화할 수 있도록 스크롤 핸들러를 requestAnimationFrame으로 감싸세요.

  5. 대규모 JSON.parse 호출 (Large JSON.parse calls) 거대한 JSON 파일을 파싱하면 메인 스레드가 차단됩니다. 이는 입력 지연(input lag)을 유발합니다. • 영향: 호출당 50-200ms의 프리징(freeze). • 해결책: Web Workers를 사용하여 메인 스레드 외부에서 데이터를 파싱하세요.

  6. 복잡한 CSS 선택자 매칭 (Complex CSS selector matching) 깊게 중첩되거나 복잡한 선택자는 스타일 재계산 속도를 늦춥니다. • 영향: 스타일 계산 시간 5-15% 증가. • 해결책: CSS 구조를 단순화하고 더 평탄한(flatter) 선택자를 사용하세요.

  7. 중복된 번들 청크 (Duplicate bundle chunks) 최적화되지 않은 번들은 대역폭을 낭비합니다. • 영향: 100-500KB의 불필요한 데이터 전송. • 해결책: webpack-bundle-analyzer와 같은 도구를 사용하여 중복 코드를 찾아 제거하세요.

진행 상황을 측정하는 방법: • Chrome DevTools를 열고 Performance 탭으로 이동합니다. • 5초 동안 세션을 기록합니다. • Main flame chart에서 긴 작업(long tasks)을 찾습니다. • 한 가지 해결책을 적용한 후 Rendering 및 Painting 시간을 비교합니다.

Core Web Vitals 점수를 개선하려면 이 영역들에 집중하세요.

출처: https://dev.to/kui_luo/how-to-find-and-fix-7-hidden-performance-bottlenecks-in-your-javascript-code-ek5