Published on

Blink Paint Flashing로 리페인트 원인 추적

Authors

웹 성능 문제를 디버깅하다 보면 “CPU는 바쁘고, 스크롤은 버벅이고, 팬이 도는데 정작 네트워크는 조용한” 상황을 자주 만납니다. 이때 범인은 의외로 단순합니다. 화면이 필요 이상으로 자주 다시 그려지는 리페인트(repaint)입니다.

리페인트 자체는 나쁜 일이 아닙니다. 문제는 작은 UI 변화 하나가 페이지 전체 리페인트로 번지거나, 스크롤 중에 계속 리페인트가 발생해 메인 스레드가 페인팅에 시간을 잡아먹는 경우입니다. Chrome DevTools의 Blink Paint Flashing은 이런 “어디가 얼마나 자주 다시 칠해지는지”를 즉시 시각화해 주는 최고의 1차 도구입니다.

이 글에서는 Blink Paint Flashing으로 리페인트를 발견하고, DevTools의 Rendering/Layers/Performance를 엮어 원인을 추적한 뒤, 실제로 줄이는 방법까지 한 번에 정리합니다.

관련해서 렌더링 병목을 더 넓게 추적하는 방법은 Chrome LCP 느림? Render‑Blocking 리소스 추적법도 함께 보면 전체적인 최적화 흐름을 잡는 데 도움이 됩니다.

리페인트·리플로우·컴포지팅을 빠르게 구분하기

원인 추적 전에 용어를 정리하면 디버깅 속도가 빨라집니다.

  • 리페인트(repaint): 픽셀을 다시 그리는 작업. 예: background-color, color, box-shadow 변경 등
  • 리플로우(reflow) 또는 레이아웃(layout): 요소의 크기/위치 계산을 다시 하는 작업. 예: width, height, top/left, DOM 추가로 레이아웃이 바뀌는 경우
  • 컴포지팅(compositing): 레이어를 합성해 화면에 뿌리는 단계. transform, opacity는 레이아웃/페인트를 피하고 합성만으로 처리되는 경우가 많음

현대 브라우저 파이프라인은 단순히 “리페인트가 발생했다”에서 끝나지 않고, 어떤 변경이 Layout까지 유발하는지, 또는 Paint만 하는지, 더 나아가 Composite만으로 끝나는지가 핵심입니다.

  1. Chrome에서 페이지 열기
  2. DevTools 열기: F12 또는 Ctrl+Shift+I (macOS는 Cmd+Option+I)
  3. Command Menu 열기: Ctrl+Shift+P (macOS는 Cmd+Shift+P)
  4. Rendering 입력 후 Show Rendering 선택
  5. Rendering 패널에서 Paint flashing 체크

이제 화면에서 리페인트가 발생하는 영역이 초록색(환경에 따라 색상 차이 가능)으로 번쩍입니다.

관찰 포인트: “정상적인 깜빡임” vs “의심스러운 깜빡임”

  • 정상에 가까운 패턴
    • 입력창 타이핑 시 입력창 주변만 깜빡임
    • hover 효과가 걸린 버튼만 깜빡임
  • 의심해야 할 패턴
    • 커서만 움직였는데 페이지 대부분이 깜빡임
    • 스크롤할 때 고정 헤더/사이드바가 계속 깜빡임
    • 애니메이션이 돌아가는 동안 큰 영역이 매 프레임 깜빡임

Paint flashing은 “어디가 다시 칠해지는지”를 알려줄 뿐, “왜”인지는 알려주지 않습니다. 다음 단계에서 원인을 좁힙니다.

1차 원인 좁히기: 어떤 이벤트가 트리거인가

먼저 “무엇을 할 때” 리페인트가 폭증하는지 상황을 고정합니다.

  • 스크롤 중에만 발생하는가
  • 마우스 이동(hover)에서 발생하는가
  • 특정 컴포넌트 상태 변경(토글, 탭 전환)에서 발생하는가
  • 타이머/애니메이션(예: setInterval)에서 발생하는가

이렇게 트리거를 정한 뒤, DevTools Performance로 증거를 남깁니다.

Performance로 Paint가 어디서 발생하는지 캡처하기

  1. DevTools Performance 탭으로 이동
  2. Record 버튼 클릭
  3. 문제 동작(스크롤/호버/애니메이션 등)을 3~5초 수행
  4. Stop 후 타임라인 분석

타임라인에서 다음을 확인합니다.

  • Main 스레드에 Recalculate Style/Layout/Paint가 반복되는지
  • Paint가 길게(두껍게) 나타나는 구간이 있는지
  • 프레임 타임이 16ms(60fps 기준)를 자주 넘는지

팁: “Paint만 많은지, Layout까지 많은지”를 먼저 가르기

  • Layout이 함께 많으면
    • DOM 측정(getBoundingClientRect, offsetHeight)과 스타일 변경이 섞여 강제 동기 레이아웃이 발생했을 가능성이 큼
  • Paint만 많으면
    • 시각 효과(box-shadow, filter, 큰 반투명 영역)나 스크롤 고정 요소의 페인팅 비용이 큰 경우가 많음

Layers 패널로 “왜 이게 계속 칠해지지?”를 확인

리페인트 최적화의 핵심은 “불필요하게 큰 영역이 하나의 페인팅 단위가 되지 않게” 만드는 것입니다. 이를 위해 레이어 분리가 되어 있는지 확인해야 합니다.

  • DevTools Command Menu에서 Layers를 열어 확인
  • 고정 헤더, 떠있는 툴바, 모달, 애니메이션 요소가 별도 레이어로 승격되어 있는지 확인

레이어가 적절히 분리되면 스크롤 시 본문만 움직이고, 고정 요소는 합성으로 처리되어 페인트가 줄어드는 경우가 많습니다.

다만 레이어 승격은 만능이 아닙니다. 레이어가 너무 많아지면 메모리와 합성 비용이 증가합니다. “문제 구간에만 최소한으로” 적용하는 게 원칙입니다.

자주 나오는 리페인트 원인 7가지와 해결 패턴

아래는 Paint flashing에서 흔히 발견되는 원인과, 해결을 위한 대표 패턴입니다.

1) box-shadow/filter: blur() 같은 고비용 페인트

큰 영역에 그림자/블러가 적용되면 스크롤이나 애니메이션 중 페인트 비용이 급격히 커집니다.

  • 해결 방향
    • 그림자 범위를 줄이거나 단순화
    • 가능한 경우 filter 대신 이미지/그라데이션으로 대체
    • 애니메이션 중에는 효과를 제거하거나 강도를 낮춤
/* 비용이 큰 경우가 많은 패턴 */
.card {
  box-shadow: 0 20px 60px rgba(0,0,0,0.25);
  filter: blur(0px);
}

/* 대안: 그림자 단순화 */
.card {
  box-shadow: 0 8px 24px rgba(0,0,0,0.16);
}

2) background-attachment: fixed로 인한 스크롤 페인트

특정 환경에서 스크롤 시 계속 페인트를 유발할 수 있습니다.

  • 해결 방향
    • 고정 배경이 꼭 필요하면 별도 레이어/이미지 전략 검토
    • 모바일에서는 조건부로 끄기
.hero {
  background-image: url('/bg.jpg');
  background-attachment: fixed;
}

@media (max-width: 768px) {
  .hero {
    background-attachment: scroll;
  }
}

3) top/left 애니메이션으로 Layout과 Paint를 유발

위치 이동을 top/left로 하면 레이아웃이 자주 깨지고 페인트까지 동반됩니다.

  • 해결 방향
    • transform: translate3d(...)로 이동
/* 나쁜 예: 레이아웃 유발 가능 */
.toast {
  position: fixed;
  top: 20px;
  transition: top 200ms ease;
}
.toast.open {
  top: 60px;
}

/* 좋은 예: 합성으로 처리될 확률이 큼 */
.toast {
  position: fixed;
  transform: translate3d(0, 0, 0);
  transition: transform 200ms ease;
}
.toast.open {
  transform: translate3d(0, 40px, 0);
}

4) 스크롤 이벤트에서 매번 스타일 변경

스크롤 핸들러에서 DOM 스타일을 계속 만지면 리페인트가 폭증합니다.

  • 해결 방향
    • requestAnimationFrame으로 배치
    • 읽기(측정)와 쓰기(스타일 변경) 분리
    • 가능하면 position: sticky 등 CSS로 대체
let ticking = false;

window.addEventListener('scroll', () => {
  if (ticking) return;
  ticking = true;

  window.requestAnimationFrame(() => {
    const y = window.scrollY;
    document.documentElement.dataset.scrolled = String(y > 10);
    ticking = false;
  });
}, { passive: true });
/* JS로 height/box-shadow를 계속 바꾸기보다 상태만 토글 */
:root[data-scrolled="true"] .header {
  box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}

5) 큰 고정 요소가 본문과 함께 계속 페인트

position: fixed 요소가 스크롤 중 계속 깜빡이면, 레이어 분리가 제대로 안 되었거나 해당 요소가 페인트 비용이 큰 스타일을 포함하고 있을 수 있습니다.

  • 해결 방향
    • 정말 필요한 경우에만 will-change: transform을 제한적으로 사용
    • fixed 요소 내부의 backdrop-filter, 큰 반투명 배경 등을 줄이기
/* 남발 금지: 문제 요소에만, 짧게 */
.header {
  will-change: transform;
}

Safari 계열에서는 합성/레이어 관련 이슈가 더 자주 보입니다. 크로스 브라우저 관점에서의 함정과 대응은 Safari iOS 스크롤 잔상·깜빡임 해결 - Compositing 튜닝도 참고할 만합니다.

6) 이미지/캔버스/비디오 위에 반투명 오버레이

반투명 레이어가 큰 영역을 덮으면 페인트/합성 비용이 상승할 수 있습니다.

  • 해결 방향
    • 오버레이 영역 축소
    • 정적인 경우 미리 합성된 리소스로 대체
    • 필요 시 레이어 분리 검토
.overlay {
  background: rgba(0,0,0,0.35);
}

7) DOM 측정과 스타일 변경이 섞여 강제 레이아웃 발생

아래처럼 “읽기 후 쓰기”가 섞이면 브라우저가 즉시 레이아웃을 확정해야 해서 Layout/Paint가 연쇄로 발생할 수 있습니다.

// 나쁜 예: 레이아웃 스래싱 가능
function onResize() {
  const h = el.getBoundingClientRect().height; // read
  el.style.height = (h + 10) + 'px';          // write
  const w = el.getBoundingClientRect().width; // read again
  el.style.width = (w + 10) + 'px';           // write again
}
// 개선: 읽기와 쓰기를 분리
function onResize() {
  const rect = el.getBoundingClientRect();
  const nextH = rect.height + 10;
  const nextW = rect.width + 10;

  el.style.height = nextH + 'px';
  el.style.width = nextW + 'px';
}

실전 디버깅 루틴: Paint Flashing에서 “원인 코드”까지

다음 순서로 하면 재현 가능한 케이스에서 원인까지 빠르게 도달합니다.

1) Paint Flashing으로 “범위” 확인

  • 작은 영역만 깜빡이는지
  • 페이지 절반 이상이 깜빡이는지
  • 스크롤/호버/애니메이션 중 언제 발생하는지

2) Performance로 “작업 종류” 확인

  • Layout이 동반되는지
  • Paint가 과도한지
  • Long Task가 끼어 있는지

메인 스레드가 길게 막히는 문제까지 함께 의심되면 Chrome INP 점수 급락 원인 - Long Task 추적법처럼 Long Task 관점으로도 병행 분석하는 것이 좋습니다.

3) 문제 요소를 좁히는 방법

  • DevTools Elements에서 해당 영역의 DOM을 찾아 hover 해보고(Elements hover 하이라이트)
  • 문제 요소에 임시로 스타일을 껐다 켜며 영향도를 확인
    • 예: box-shadow 제거, filter 제거, 반투명 배경 제거
  • 스크롤 핸들러/타이머가 있으면 임시로 주석 처리 후 비교

4) 수정 후 다시 Paint Flashing으로 검증

최적화는 “됐다”가 아니라 “줄었다”를 확인해야 합니다.

  • 깜빡임 면적이 줄었는지
  • 깜빡임 빈도가 줄었는지
  • Performance에서 Paint/Layout 시간이 줄었는지

체크리스트: 리페인트를 줄이는 설계 원칙

  • 애니메이션은 가능한 transform/opacity 중심으로 설계
  • 스크롤 이벤트에서 DOM 스타일 변경 최소화, 필요 시 requestAnimationFrame 배치
  • 큰 요소에 box-shadow/filter/반투명 오버레이를 남발하지 않기
  • 고정 헤더/사이드바는 불필요한 스타일을 줄이고 레이어 전략을 점검
  • will-change는 “문제 요소에만” 제한적으로 사용하고, 남발하지 않기

마무리

Blink Paint Flashing은 리페인트 최적화의 출발점입니다. 화면이 깜빡이는 위치를 “증거”로 확보한 다음, PerformanceLayout/Paint/Composite 중 어디가 병목인지 확인하고, Layers로 레이어 분리 상태를 점검하면 원인을 코드 레벨까지 빠르게 좁힐 수 있습니다.

리페인트는 눈에 보이는 현상이라 디버깅이 쉬운 편이지만, 해결은 UI 구현 습관(애니메이션 방식, 스크롤 처리, 스타일 선택)과 직결됩니다. 오늘 소개한 루틴대로 한 번만 제대로 잡아두면, 이후에는 “버벅임”을 재현하고 고치는 속도가 체감될 정도로 빨라질 것입니다.