화면이 빨리 뜨고 뜨는 동안 덜컹거리지 않는 것, 사용자 입장에서는 그게 곧 좋은 경험입니다. 성능은 서버만의 일이 아니라 마크업과 CSS 한 줄로도 크게 갈립니다. 브라우저에 바로 붙여 확인할 수 있는 코드로 하나씩 짚어 보겠습니다.
29.1 화면 밖 이미지는 늦게 부르기
글 아래쪽에 있어서 아직 보이지도 않는 이미지를 처음부터 다 받아 오면 첫 화면이 느려집니다. 마크업 속성 하나로 보일 때까지 미룰 수 있습니다.
<img src="photo-1.jpg" alt="후기 사진" width="800" height="600">
<img src="photo-2.jpg" alt="후기 사진" width="800" height="600" loading="lazy">
두 번째 이미지는 스크롤이 그 근처에 닿을 때 비로소 내려받습니다. 첫 화면에 필요한 이미지는 그대로 두고, 한참 아래 것들에만 loading="lazy" 를 붙이는 게 요령입니다.
29.2 자리를 먼저 잡아 흔들림 막기
이미지가 뒤늦게 도착하면서 아래 글을 밀어내면, 읽던 줄이 갑자기 튀어 오릅니다. 이 레이아웃 흔들림은 도착 전에 자리를 예약해 두면 사라집니다.
.thumb {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
가로세로 비율을 미리 정해 두면 브라우저가 그만큼의 빈 상자를 먼저 그려 둡니다. 이미지가 늦게 와도 상자 안만 채워질 뿐, 아래 문단은 꿈쩍하지 않습니다.
29.3 화면 크기에 맞는 이미지만 받기
휴대폰에 2000px짜리 원본을 보내는 건 데이터 낭비이자 느림의 원인입니다. 여러 크기를 등록해 두고 기기가 알아서 고르게 합니다.
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 800px"
alt="배너">
브라우저는 화면 폭과 화면 밀도를 보고 srcset 목록에서 가장 알맞은 파일을 고릅니다. 작은 화면에는 작은 파일이 가니, 같은 화면인데도 훨씬 가볍게 뜹니다.
29.4 폰트가 글자를 가리지 않게
웹폰트를 받는 동안 글자가 아예 안 보이면 사용자는 빈 화면을 마주합니다. 폰트가 늦어도 기본 글꼴로 먼저 읽히게 해 줍니다.
@font-face {
font-family: "Pretendard";
src: url("/fonts/pretendard.woff2") format("woff2");
font-display: swap;
}
font-display: swap 은 폰트가 도착하기 전까지 시스템 글꼴로 글을 보여 주고, 준비되면 조용히 바꿔 끼웁니다. 내용을 읽는 데 한순간의 공백도 없습니다.
29.5 화면 밖 영역은 그리기 건너뛰기
글이 아주 긴 페이지는 아직 보이지도 않는 아래쪽까지 브라우저가 미리 계산하느라 버벅입니다. 화면 밖 구역은 렌더링을 미루게 할 수 있습니다.
.section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}
화면에 들어오기 전까지 그 구역의 내부를 그리지 않아 초기 렌더가 빨라집니다. contain-intrinsic-size 로 예상 높이를 일러 주면 스크롤바 길이도 튀지 않습니다.
29.6 그리기 범위를 가두기
어느 카드 하나가 바뀌었는데 페이지 전체를 다시 계산하면 손해입니다. 변화의 영향을 그 상자 안으로 가둬 둘 수 있습니다.
.widget {
contain: layout paint;
}
contain: layout paint 는 이 요소 안의 변화가 바깥 레이아웃에 번지지 않는다고 브라우저에 약속하는 것입니다. 덕분에 다시 그릴 범위가 위젯 하나로 좁혀집니다.
29.7 will-change는 아껴 쓰기
애니메이션을 매끄럽게 하려고 will-change 를 아무 데나 붙이면 오히려 메모리를 잡아먹어 느려집니다. 정말 곧 움직일 요소에만, 움직이기 직전에만 씁니다.
.sheet {
transition: transform .25s ease;
}
.sheet.is-opening {
will-change: transform;
}
평소엔 없다가 열리는 순간에만 힌트를 주는 방식입니다. 상시로 걸어 두는 대신 필요한 찰나에만 켜서, 이득은 챙기고 부담은 피합니다.
29.8 정말 급한 자원만 미리 부르기
첫 화면을 좌우하는 큰 배너 이미지는 CSS를 다 읽은 뒤에야 발견되어 늦게 뜨곤 합니다. 이런 핵심 자원은 브라우저에 미리 귀띔해 줄 수 있습니다.
<link rel="preload" as="image" href="/img/hero-800.jpg">
<link rel="preload" as="font" href="/fonts/pretendard.woff2"
type="font/woff2" crossorigin>
preload 는 정말 급한 몇 개에만 씁니다. 이걸 남발하면 우선순위가 뒤엉켜 오히려 다 같이 늦어지니, 첫 화면의 주인공 한둘로 아껴야 합니다.
29.9 이미지 디코딩을 비켜 세우기
큰 이미지는 화면에 그리기 전 압축을 푸는 디코딩 과정에서 잠깐 멈춤을 만듭니다. 이 작업을 본 흐름 옆으로 비켜 세울 수 있습니다.
<img src="gallery.jpg" alt="갤러리" decoding="async"
width="1200" height="800">
decoding="async" 는 이미지 해제를 다른 렌더링과 겹쳐 처리하게 해, 스크롤이나 입력이 그 사이에 뚝 끊기지 않게 합니다. width 와 height 를 함께 적어 자리까지 잡아 두면 더욱 안정적입니다.
29.10 첫 화면 주인공엔 우선순위 올리기
같은 이미지라도 첫 화면을 채우는 대표 그림은 남들보다 먼저 받아야 합니다. 브라우저에 이건 급하다고 등급을 매겨 줄 수 있습니다.
<img src="hero-800.jpg" alt="대표 이미지"
fetchpriority="high" width="800" height="450">
<img src="thumb.jpg" alt="목록 썸네일"
fetchpriority="low" loading="lazy">
fetchpriority="high" 는 대표 이미지를 먼저 당겨 오게 하고, 덜 급한 썸네일엔 low 를 줘 뒤로 미룹니다. 같은 자원이라도 차례를 바꾸면 첫인상이 달라집니다.
29.11 늦게 채워질 목록엔 자리 예약
데이터를 불러와 채우는 목록은 응답이 오는 순간 페이지가 훌쩍 늘어나며 아래를 밀어냅니다. 채워지기 전에 최소 높이를 잡아 두면 이 흔들림을 막습니다.
.feed {
min-height: 400px;
}
.skeleton {
height: 72px;
background: #f1f5f9;
border-radius: 8px;
margin-bottom: 8px;
}
목록 자리에 미리 min-height 를 주고 회색 뼈대를 깔아 두면, 실제 데이터가 그 자리를 조용히 대체합니다. 사용자는 빈 화면 대신 곧 채워질 윤곽을 보게 됩니다.
29.12 정리
성능은 거창한 최적화보다 이런 작은 습관들이 쌓여 만들어집니다. 늦춰도 될 건 늦추고, 자리는 미리 잡고, 급한 것만 앞세우는 원칙만 지켜도 화면은 눈에 띄게 가벼워집니다. 빠른 화면은 그 자체로 사용자에게 건네는 배려입니다.