글실험 기록
창 하나 여는 데 283ms: UX/UI 갤러리가 버벅이던 두 가지 이유
움직이는 요소 400개가 한 페이지에 있는 갤러리에서 릴스 보기와 프롬프트 창이 버벅였습니다. 크롬 성능 기록으로 원인을 찾아 283ms를 33~50ms로 줄인 과정입니다.

Lucian UX/UI Prompt는 버튼, 차트, 폼 같은 인터페이스 요소 400개를 한 페이지에서 직접 눌러 볼 수 있는 갤러리입니다. 카드마다 실제로 동작하는 작은 시연이 들어 있고, 화면에 보이면 저절로 움직입니다.
이 갤러리에는 두 가지 보기가 더 있습니다. 요소를 한 장씩 크게 넘겨 보는 릴스 보기, 그리고 요소의 적용 프롬프트와 코드를 띄우는 프롬프트 창입니다. 10월 12일, 둘 다 열 때 화면이 잠깐 멈칫한다는 걸 알아챘습니다. 눈으로 느껴질 정도면 보통 100ms를 넘습니다.
먼저 잰다
감으로 고치기 전에 크롬 개발자 도구의 성능 기록(Performance trace)으로 릴스를 여는 순간을 녹화했습니다. 측정은 헤드리스 크롬에서 같은 동작을 반복하는 스크립트로 했습니다. GPU 없이 소프트웨어로 그리는 환경이라 실제 기기보다 숫자가 크게 나오지만, 고치기 전과 후를 같은 조건에서 비교하는 데는 충분합니다.
| 동작 | 가장 긴 프레임 |
|---|---|
| 릴스 처음 열기 | 283ms |
| 릴스 닫기 | 167ms |
| 릴스 다음 장 | 117ms |
| 프롬프트 창 열기 | 75ms |
60fps를 유지하려면 한 프레임이 16.7ms 안에 끝나야 합니다. 283ms면 17프레임 가까이 화면이 멈춘 셈입니다.
원인 1: 릴스를 열 때 모든 슬라이드를 한꺼번에 준비하고 있었다
성능 기록에서 릴스를 여는 순간 172ms짜리 긴 작업이 보였습니다. 그 안에서 스타일 다시 계산(Update Layout Tree)이 94ms, 이벤트 처리가 103ms를 차지했고, 모두 현재 장을 정하는 함수 하나에서 시작됐습니다.
코드를 따라가 보니 릴스를 여는 순간 모든 슬라이드의 틀을 만들고, 각 슬라이드에 시연을 붙이고 있었습니다. 사용자는 한 장만 보는데 브라우저는 수백 장을 준비했습니다. 게다가 릴스 뒤에 깔린 원래 갤러리의 카드들도 계속 움직이고 있었습니다.
세 가지를 바꿨습니다.
- 보이는 장 근처만 채운다. 슬라이드는 빈 틀만 만들어 두고, 현재 장 앞뒤 2장까지만 내용을 채웁니다. 4장 이상 멀어진 슬라이드는 내용을 비웁니다.
- 뒤에 깔린 갤러리는 잠시 뒤에 내린다. 릴스가 열리는 전환 효과가 끝난 뒤(700ms) 뒤쪽 갤러리의 시연을 내립니다. 전환 중에 무거운 작업이 겹치지 않게 하려는 것입니다.
- 안 보이는 슬라이드의 배경 애니메이션은 멈춘다.
.g-slide:not(.is-on) .g-slide-bg { animation-play-state: paused; }| 동작 | 전 | 후 |
|---|---|---|
| 릴스 처음 열기 | 283ms | 33~50ms |
| 릴스 닫기 | 167ms | 17ms |
| 릴스 다음 장 | 117ms | 17ms |
| 동시에 돌던 애니메이션 | 202개 | 7개 |
동시에 돌던 애니메이션 수가 가장 극적으로 줄었습니다. 화면에 보이지 않는 것 195개가 계속 움직이고 있었던 겁니다.
원인 2: 프롬프트 창 뒤의 흐림 효과
프롬프트 창은 열릴 때 뒤 화면을 흐리게 덮습니다. CSS로는 대화상자 뒤 배경(::backdrop)에 backdrop-filter: blur(10px)를 건 것입니다. 보기에는 좋지만, 그 뒤에는 수십 개의 시연이 움직이고 있었습니다.
어느 쪽이 원인인지 확인하려고 조건을 하나씩 바꿔 가며 쟀습니다.
| 조건 | 가장 긴 프레임 | 50ms 넘는 프레임 |
|---|---|---|
| 그대로 (흐림 + 뒤 애니메이션) | 75ms | 11개 |
| 뒤 애니메이션만 멈춤 | 75ms | 6개 |
| 흐림만 제거 | 34ms | 0개 |
흐림을 끄자 바로 해결됐습니다. 성능 기록에서도 자바스크립트 실행은 거의 없고, 화면을 합성하는 그래픽 작업이 대부분을 차지했습니다. 흐림 효과는 뒤에 있는 모든 픽셀을 매 프레임 다시 읽어 섞어야 합니다. 뒤 화면이 움직이면 이 계산을 계속 반복합니다.
흐림을 빼는 대신 페이지 배경색을 84% 불투명도로 덮었습니다. 뒤가 충분히 가려져서 창에 집중되는 느낌은 거의 같습니다.
/* 전 */
.g-dlg::backdrop { backdrop-filter: blur(10px) saturate(170%); }
/* 후 */
.g-dlg::backdrop { background: color-mix(in srgb, var(--ux-page) 84%, transparent); }함정: :has()가 느렸다
창이 열려 있는 동안에는 뒤의 시연도 멈추고 싶었습니다. 처음에는 CSS만으로 깔끔하게 처리했습니다.
body:has(dialog[open]) .g-slide-bg { animation-play-state: paused; }그런데 이 규칙을 넣자 창을 여는 데 다시 100ms 가까이 걸렸습니다. :has()는 자식 상태에 따라 부모를 고르는 선택자라, 대화상자가 열리고 닫힐 때마다 문서 전체의 스타일을 다시 확인하게 됩니다. 요소가 수천 개인 페이지에서는 이 비용이 큽니다.
그래서 창을 열고 닫는 코드에서 body에 클래스를 직접 붙이고 떼는 방식으로 바꿨습니다.
dialog.addEventListener('close', () => document.body.classList.remove('has-dlg'));
// 열 때: document.body.classList.add('has-dlg'); dialog.showModal();body.has-dlg .g-slide-bg { animation-play-state: paused; }최종 결과는 창 열기 33~42ms, 닫기 33ms, 50ms를 넘는 프레임 0개입니다.
정리
| 문제 | 원인 | 해결 |
|---|---|---|
| 릴스 열기 283ms | 보이지 않는 슬라이드까지 전부 준비 | 현재 장 앞뒤 2장만 채우고, 뒤 갤러리는 전환 후 내림 |
| 프롬프트 창 75ms | 움직이는 화면 위의 흐림 효과 | 흐림 대신 84% 반투명 덮개 |
| 창 열기 100ms로 다시 느려짐 | 문서 전체에 걸리는 :has() | body 클래스 토글 |
세 문제 모두 결국 보이지 않는 곳에서 일하고 있었다는 점에서 같았습니다. 화면에 안 보이는 슬라이드, 덮개 뒤에서 움직이는 시연, 바뀔 때마다 전체를 다시 훑는 선택자. 무엇을 그릴지보다 무엇을 그리지 않을지 정하는 쪽이 효과가 컸습니다.
숫자는 헤드리스 크롬의 소프트웨어 렌더링 기준이라, GPU가 있는 실제 기기에서는 전후 모두 더 작게 나옵니다. 비율로 보면 릴스 열기는 5배 넘게, 창 열기는 약 2배 빨라졌습니다.