기술 블로그

Chrome background-colorGPU 컴포지터 렌더 버그 대응 본문

카테고리 없음

Chrome background-colorGPU 컴포지터 렌더 버그 대응

jaegwan 2026. 11. 6. 17:20
반응형

hover 가 끝난 행이 한 프레임 번쩍이는 이유

2026-11-06

Chrome 은 CSS 배경색 전환을 GPU 합성기에서 칠하는데, 전환이 끝나는 순간 가끔 한 프레임 동안 배경만 시작 색으로 되돌아간다. 배경색 대신 덮개의 opacity 를 전환하면 사라진다.

증상

언호버 페이드가 다 끝나 흰색으로 돌아간 행이, 한 프레임 동안 다시 hover 파랑으로 번쩍였다. 새소식 목록은 행에 마우스를 올리면 배경이 흰색에서 파랑(#0fa3ff)으로 0.3초 페이드되고, 내리면 다시 흰색으로 0.3초 페이드되는 단순한 hover 다.

.row {
  transition: background-color 0.3s ease;
}
.row:hover {
  background: #0fa3ff;
  .title { color: #fff; }
}
  • 한참 전에 커서가 떠난 행에서도 번쩍였다. 그래서 처음에는 hover 와 무관한 문제처럼 보였다.
  • 매번은 아니고 가끔이었다. 여러 행을 지나가거나, 목록 밖으로 커서를 뺄 때 잘 보였다.
  • Chrome DevTools Performance 로 잡아 보면, 행의 color · transform · background-color 전환과 썸네일 opacity 전환이 같은 순간에 끝나고, 그 직후 프레임에서 번쩍였다.

Performance 기록. 언호버 페이드가 끝난 뒤 필름스트립 한 칸에서 마지막 행이 다시 파랗다.

번쩍이는 지점을 확대한 애니메이션 트랙. 다섯 전환이 한 프레임(16.7ms) 안에서 함께 끝난다.

헛짚은 가설들

세 가지를 먼저 의심했고, 각각 실제 문제였지만 이번 번쩍임의 원인은 아니었다. 모두 같은 0.3초 안에서 일어나서 눈으로는 구분이 안 됐다.

가설 확인 방법 결과
행마다 하나씩 있던 썸네일이 행을 옮길 때 두 장 겹침 rAF 마다 두 썸네일의 opacity · 위치 기록 실제로 겹쳤다. 목록에 썸네일 한 장만 두어 고쳤지만 번쩍임은 남았다
글자색과 배경색이 반대 방향으로 동시에 페이드되며 대비가 0 에 가까워짐 프레임별 글자 · 배경 대비비 측정 중간에 대비가 1.03 까지 떨어져 글자가 잠깐 묻힌다. 하지만 이미 떠난 행에서도 번쩍인 건 설명 못 한다
hover 와 무관한 행이 GPU 레이어 문제로 다시 그려짐 마지막 hover 변화 뒤 0.4초 지난 행의 프레임 간 픽셀 변화 소프트웨어, GPU 렌더링 모두 0건

두 번째 가설은 "이미 언호버한 지 한참 지난 행에서 깜빡인다" 는 관찰로 기각됐다. 결정적인 단서는 Performance 기록에서 나왔다. 언호버 애니메이션이 완료된 뒤에 깜빡인다는 것이다.

재현과 측정

번쩍인 프레임에서는 배경만 전환 시작값이었고, 글자색 · 날짜색 · 내용 위치는 전부 끝값이었다. 그 순간에도 getComputedStyle 의 배경색은 투명이었다. CSS 상태는 맞는데 화면만 틀린 것이다.

측정은 Playwright 로 설치된 Chrome 을 GPU 래스터 옵션으로 헤드리스 실행해서 했다.

  1. 마지막 행에 hover 한 뒤 아래, 오른쪽, 위로 커서를 빼는 동작을 반복했다.
  2. CDP Page.startScreencast 로 실제 합성된 프레임을 모두 받았다.
  3. 프레임마다 행 왼쪽 여백 한 픽셀의 파랑 정도를 재서, 페이드가 0 에 닿은 뒤 다시 올라가는 프레임을 찾았다.
  4. 같은 시각 rAF 로 getComputedStyle 과 :hover 를 기록해 스타일이 되돌아갔는지 대조했다.

픽셀 값은 이렇게 나왔다. 240 이 hover 파랑, 0 이 흰색이다.

240,240,231,213,187,157,128,102,80,62,47,36,25,18,12,8,4,2,0,240,0,0,0

 

측정에서 잡힌 번쩍인 프레임. 마지막 행의 배경은 hover 파랑인데, 제목은 어두운 색이고 내용도 오른쪽으로 밀리지 않았다. 배경만 시작 색으로 돌아간 것이다.

같은 동작을 조건마다 18번씩 반복했다. 횟수가 적어 방향만 보는 값이다.

조건 번쩍인 횟수 / 18
원래 코드 5
썸네일 숨김 3
썸네일 will-change 제거 1
Chrome 을 --disable-features=CompositeBGColorAnimation 으로 실행 0
배경색 대신 덮개 opacity 전환 0

동시에 끝나는 다른 전환을 줄이면 덜 생기지만, 없어지는 건 Chrome 의 배경색 합성 기능을 끄거나 배경색 전환을 안 쓸 때뿐이었다.

Chromium 내부: 배경색 전환은 합성기가 칠한다

transform · opacity 와 달리 배경색 전환은 메인 스레드와 합성기가 나눠 맡는다. CompositeBGColorAnimation 기능이 켜져 있을 때의 이야기이고, 소스에서 확인한 사실은 네 가지다.

  1. 전환이 시작되면 메인 스레드는 배경 자리에 "나중에 칠할 그림" 표시만 기록한다. 이때 그 순간의 배경색을 MainThreadValue 로 같이 넣는다.
  2. 합성기는 매 프레임 0 에서 1 로 가는 진행률을 받아 그 자리에 색을 보간해 칠한다.
  3. 진행률 값이 없으면 기록해 둔 MainThreadValue 로 칠한다.
  4. 합성기는 메인 스레드가 커밋할 때마다, 직전 커밋 이후 애니메이션이 건드리지 않은 진행률 항목을 지운다.
// background_color_paint_definition.cc 의 Sample(), 일부 줄임
Color color = input->MainThreadValue();
if (!animated_property_values.empty()) {
  std::optional<double> progress = entry->second.float_value;
  if (progress) {
    color = input->Interpolate(progress.value());
  }
}
return color;

언호버 전환에서 MainThreadValue 는 시작 색, 즉 hover 파랑이다. 아래 순서가 맞아떨어지면 끝난 행이 한 프레임 파랗게 칠해진다.

⑤ 에서 진행률이 지워진 뒤 실제로 그 표시가 칠해지는 경로는 소스를 끝까지 따라가 확인하지 못했다. 진행률 보관 코드에는 마지막 값을 유지하는 장치도 있다. 다만 번쩍인 프레임이 "배경만 정확히 시작 색" 이고, 기능을 끄면 0번이 된다는 점은 이 구조와 맞는다. 글자색 같은 다른 전환이 많을수록 ④ 가 끼어들 기회가 늘어, 전환이 여럿 겹칠 때 잘 생긴 것도 설명된다.

해결: 배경색 대신 덮개 opacity

행 배경색은 전환하지 않고, 행 안에 파란 덮개를 깔아 그 opacity 를 전환했다. opacity 는 합성기가 레이어 속성만 바꾸는 속성이라, 끝난 뒤 다시 그리기를 기다리는 바통 넘기기가 없다.

@media (hover: hover) {
  .row::before {
    content: "";
    position: absolute;   // .row 는 이미 position: relative
    inset: 0;
    background: #0fa3ff;
    opacity: 0;
    transition: opacity 0.3s ease;
    pointer-events: none;
  }
  // 덮개보다 뒤 순서의 positioned 요소로 두어 덮개 위에 그린다
  .content,
  .arrow {
    position: relative;
  }
  .row:hover::before {
    opacity: 1;
  }
}
  • z-index 를 쓰지 않았다. ::before 는 행의 첫 자식이다. 뒤따르는 내용과 화살표에 position: relative 만 주면, 같은 층에서 문서 순서대로 덮개 위에 그려진다.
  • 보이는 결과는 같다. 페이드 곡선을 프레임별 픽셀로 재 보니 이전과 값이 거의 같았다. 1920px, 768px hover 화면도 같았다.
  • 번쩍임은 18번 중 0번이 됐다.

다른 방법도 있다. Chromium 은 요소나 조상에 픽셀을 움직이는 필터(blur 등)가 있으면 배경색 전환을 합성하지 않는다. 그래서 body { filter: blur(0) } 한 줄로 페이지 전체를 되돌릴 수 있다. 다만 body 가 position: fixed 자손의 기준이 돼 버려서, 플로팅 버튼이 있는 이 페이지에는 쓰지 않았다.

정리

transition: background-color 는 잘못된 코드가 아니다. 다만 아래 조건이 겹치면 Chrome 에서 한 프레임 번쩍임이 눈에 띈다.

  • 넓은 면이 강한 대비로 바뀐다. 화면 폭 전체 행이 흰색과 진한 파랑 사이를 오간다.
  • 같은 순간에 끝나는 다른 전환이 많다. 글자색처럼 메인 스레드가 매 프레임 다시 그리는 전환이 같이 돈다.
  • 언호버처럼 끝값이 바탕색이다. 다 끝났다고 느낀 뒤에 시작 색이 한 번 더 보여 더 눈에 띈다.

같은 페이지라도 누구는 보고 누구는 못 본다. 핵심은 두 가지다. 배경색 합성이 그 브라우저에서 켜져 있는지, 그리고 켜져 있다면 메인 스레드가 그 프레임 안에 따라오는지다.

  • 기능이 켜져 있는가. Chrome 계열에만 있는 기능이고, 버전과 설정에 따라 다르다. 소스상 강제 다크 모드, 강제 색상 모드(Windows 고대비)이거나 요소나 조상에 blur 같은 필터가 있으면 합성하지 않는다. 그런 사용자에게는 생기지 않는다.
  • 메인 스레드가 제때 따라오는가. 이 번쩍임은 메인 스레드와 합성기의 경합이다. CPU 성능, 같이 도는 작업, 확장 프로그램, 열려 있는 DevTools 가 메인 스레드 부담을 바꾼다. 모니터 주사율이 다르면 프레임 간격도 달라진다. 그래서 확률이 기기마다 다르다. 이 글의 측정에서도 18번 중 5번만 생겼다.
  • 어떻게 움직이는가. 여러 행을 훑거나 목록 밖으로 빠져나가면 동시에 끝나는 전환이 늘어 잘 생긴다. 한 행에서 천천히 나가면 거의 안 생긴다.

Chrome 이 이 기능을 일부 사용자에게만 켜는 실험 단계인지는 확인하지 못했다.

의심될 때는 Chrome 을 --disable-features=CompositeBGColorAnimation 으로 띄워 비교하면 한 번에 가려진다. 대상이면 배경색 대신 덮개의 opacity 를 전환한다. 작은 버튼처럼 비슷한 색끼리 바뀌는 곳은 그대로 두어도 된다.

Chromium 버그 트래커에서 이 증상의 공식 이슈는 찾지 못했다. Firefox, Safari 는 확인하지 않았다.

출처

반응형
Comments