data URI의 비용
data URI는 파일을 URL 안에 넣습니다: data:image/png;base64,iVBORw0KGgo.... URL이 들어가는 곳이면 어디든 쓸 수 있으므로, 아이콘을 HTML src 속성이나 CSS background-image 안에 둘 수 있고 브라우저는 그것을 위해 따로 요청을 보내지 않습니다.
맞바꾸는 것은 다음과 같습니다.
- 크기. Base64는 페이로드를 원본의 4/3, 약 33% 크게 만듭니다(정확한 공식은 Base64 크기 오버헤드).
data:image/png;base64,접두사가 22바이트를 더합니다. - 캐싱. 인라인된 파일은 그것을 담은 문서의 일부로만 캐시됩니다. 모든 HTML 페이지에 인라인한 로고는 페이지마다 다시 내려받습니다. CSS 파일에 인라인하면 그 CSS와 함께 캐시되지만, 아이콘 하나를 바꿔도 스타일시트 전체가 무효화됩니다.
- 렌더링. 브라우저는 렌더링을 차단하는 CSS가 도착할 때까지 아무것도 그리지 못합니다. 인라인 글꼴이나 이미지 200 KB를 실은 스타일시트는 그 바이트를 내려받는 시간만큼 첫 렌더링을 늦춥니다. HTML에서는 인라인 데이터가 그 뒤의 콘텐츠를 바이트 스트림에서 더 뒤로 밀어냅니다.
- 지연 로딩 불가. data URI를 쓴
<img>에loading="lazy"를 붙여도 아무것도 아끼지 못합니다. 바이트가 이미 페이지와 함께 도착했기 때문입니다.
그 대가로 요청 하나를 아낍니다. HTTP/1.1에서는 브라우저가 호스트당 연결을 몇 개만 열었기 때문에 이것이 큰 문제였습니다. HTTP/2와 HTTP/3에서는 많은 요청이 하나의 연결을 공유하므로 절약 효과가 작습니다. 대부분 요청과 응답 헤더, 그리고 브라우저가 늦게 발견하는 파일에 대한 왕복 지연 한 번 정도입니다.
측정: gzip과 Brotli가 Base64에 미치는 영향
33%라는 수치는 압축하지 않은 텍스트 기준입니다. 대부분의 HTML과 CSS는 gzip이나 Brotli로 제공되므로 중요한 것은 압축 후 크기입니다. 직접 측정했습니다.
방법. Debian 13 패키지의 샘플 파일을 썼습니다. 16×16 PNG 아이콘(HighContrast 아이콘 테마), 48×48 RGBA PNG(hicolor 테마), 2880×1800 Xfce 파란색 배경화면 JPEG, 이를 Pillow로 품질 80에서 800×500으로 줄인 JPEG, Roboto Slab Regular WOFF2 글꼴, SVG 아이콘 두 개(479바이트 Adwaita symbolic 아이콘과 7.6 KB GNOME Debian 소용돌이)입니다. 각 파일과 그 Base64 인코딩(data: 접두사 없이 페이로드만)을 Node.js 24 zlib으로 압축했습니다. 미리 압축하는 정적 자산에 흔히 쓰는 설정인 gzip 레벨 9와 Brotli 품질 11입니다. 크기 단위는 바이트입니다.
| 파일 | 원본 | Base64 | 원본 gzip | Base64 gzip | 원본 Brotli | Base64 Brotli |
|---|---|---|---|---|---|---|
| PNG 16×16 | 281 | 376 | 285 | 306 | 285 | 284 |
| PNG 48×48 | 4,552 | 6,072 | 4,575 | 4,602 | 4,551 | 4,589 |
| JPEG 800×500 | 8,195 | 10,928 | 7,425 | 7,173 | 7,160 | 6,908 |
| JPEG 2880×1800 | 197,011 | 262,684 | 182,684 | 183,945 | 180,114 | 181,140 |
| WOFF2 글꼴 | 52,532 | 70,044 | 52,538 | 52,765 | 52,536 | 52,690 |
| SVG 아이콘 | 479 | 640 | 291 | 427 | 247 | 410 |
| SVG 소용돌이 | 7,610 | 10,148 | 3,123 | 4,398 | 2,582 | 3,823 |
숫자가 보여 주는 것은 다음과 같습니다.
이미 압축된 바이너리는 오버헤드를 거의 다 잃습니다. PNG, JPEG, WOFF2 데이터는 이미 압축되어 무작위에 가까우므로 다시 압축해도 얻는 것이 거의 없습니다(PNG 두 개는 gzip에서 오히려 조금 커졌습니다). 무작위 바이트의 Base64는 각각 8비트인 64개 기호를 쓰고, 허프만 부호화는 이를 기호당 약 6비트로 되돌립니다. 압축된 Base64는 압축된 바이너리의 96–107%, 원본 파일의 84–109%였습니다. 여기 쓴 JPEG는 그 자체로 약간 압축이 되었기 때문에 Base64 버전이 gzip으로 압축한 JPEG보다 오히려 조금 작게 나왔는데, 이 차이는 계획에 반영할 만큼 크지 않습니다. 아주 작은 16×16 PNG에서는 gzip의 고정 헤더와 트레일러 18바이트가 큰 비율로 드러나며, 더 가벼운 Brotli 프레이밍에서는 사라졌습니다.
SVG는 더 나빠집니다. SVG는 반복이 많은 텍스트여서 압축이 아주 잘 됩니다. 소용돌이는 gzip으로 7,610바이트에서 3,123바이트로 줄었습니다. Base64는 3바이트 묶음을 정렬 위치에 따라 값이 달라지는 4개 문자로 뒤섞으므로, 반복되는 부분 문자열이 더 이상 두 번 같아 보이지 않습니다. gzip으로 압축한 Base64 SVG는 gzip으로 압축한 SVG보다 41–47%, Brotli로는 48–66% 컸습니다.
URL 인코딩한 SVG와 Base64
SVG는 이미 텍스트이므로 Base64가 전혀 필요 없습니다. URL 인코딩한 data URI는 읽을 수 있는 상태를 유지합니다.
.flag {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 0v16h2V8h3l1 2h5V2h-4L9 0z'/%3E%3C/svg%3E");
}
최소한 이스케이프해야 하는 것은 %, #(URL 프래그먼트를 시작하므로), <, >, 그리고 속성을 감싸는 따옴표 문자입니다. SVG 안의 큰따옴표를 작은따옴표로 바꾸면 이스케이프하지 않아도 됩니다. ASCII가 아닌 문자는 UTF-8로 퍼센트 인코딩해야 합니다.
같은 SVG 두 개를 같은 방법으로 측정했습니다.
| SVG | 인코딩 | 크기 | Gzip | Brotli |
|---|---|---|---|---|
| 아이콘 | Base64 | 640 | 427 | 410 |
| 아이콘 | 최소 URL 인코딩 | 493 | 293 | 253 |
| 아이콘 | encodeURIComponent | 723 | 326 | 279 |
| 소용돌이 | Base64 | 10,148 | 4,398 | 3,823 |
| 소용돌이 | 최소 URL 인코딩 | 7,179 | 3,057 | 2,549 |
| 소용돌이 | encodeURIComponent | 11,594 | 3,319 | 2,750 |
“최소"는 공백을 줄이고, 큰따옴표를 작은따옴표로 바꾸고, %, #, <, >만 이스케이프한 것입니다. 원래 SVG와 거의 같은 크기로 압축되었고, 소용돌이는 공백을 줄여 바이트가 빠진 덕분에 오히려 조금 작습니다. 전체 encodeURIComponent는 압축 전에는 더 크지만 여전히 Base64보다 훨씬 잘 압축됩니다. SVG를 data URI로 도구는 두 형태를 나란히 만들어 줍니다.
도구나 API가 Base64만 받거나, SVG에 이스케이프를 신경 쓰고 싶지 않은 문자가 들어 있다면 SVG에도 Base64가 여전히 의미가 있습니다.
콘텐츠 보안 정책과 data:
콘텐츠 보안 정책(CSP)에서 'self'는 data: URL을 포함하지 않습니다. 이를 허용해야 하는 지시문마다 스킴을 적어야 합니다.
Content-Security-Policy: default-src 'self'; img-src 'self' data:; font-src 'self' data:
img-src와 font-src에 data:를 허용하는 것은 흔하고 위험이 낮습니다. script-src, default-src, object-src에 허용하는 것은 전혀 다른 문제입니다. 마크업을 삽입할 수 있는 공격자가 외부 호스트 없이 스크립트를 넣을 수 있게 되어 CSP의 의미가 상당 부분 사라집니다. 인라인 이미지가 정책에 걸린다면 img-src만 넓히세요.
브라우저는 링크, 리디렉션, 스크립트 어느 방식으로든 페이지가 최상위 창을 data: URL로 이동하는 것도 차단합니다. 따라서 data URI는 페이지에 삽입할 수는 있어도 최상위 링크로 열 수는 없습니다.
인라인이 가치 있을 때
대개 가치 있는 경우:
- 아주 작은 아이콘과 UI 이미지. 대략 1–2 KB 이하이고, 특히 첫 페인트에 필요한 것. 요청 오버헤드가 크기에서 차지하는 비율이 크고, 압축이 Base64 비용 대부분을 지워 줍니다.
- 단일 파일 HTML. 디스크에서 열어도 동작해야 하는 오프라인 보고서, 내보낸 문서, 테스트 커버리지 페이지, HTML 이메일 미리보기.
- 크리티컬 CSS. 어차피 HTML에 인라인되는 CSS라면 작은 배경 이미지도 함께 실어 보낼 수 있습니다.
- 생성된 콘텐츠. 스크립트에서 PNG로 렌더링해 HTML 보고서에 넣는 차트 등.
대개 가치 없는 경우:
- 사진과 몇 킬로바이트가 넘는 모든 것. 캐시 수명을 길게 준 별도 파일이 낫습니다.
- 여러 페이지가 공유하는 이미지. HTML 응답마다 안에 담겨 다시 내려받게 됩니다.
- 웹 글꼴. WOFF2 파일은 보통 수십 킬로바이트이고(위에서 측정한 것은 52 KB), CSS에 인라인하면 스타일시트가 렌더링을 더 오래 차단하며, 별도 글꼴 파일은 캐시하고 미리 로드할 수 있습니다.
- 스크롤해야 보이는 모든 것.
loading="lazy"를 쓴 별도 파일은 필요해질 때까지 비용이 없습니다.
이메일은 별개의 경우입니다. 메일 클라이언트마다 data: 이미지 지원이 다르므로 받는 사람들이 실제로 쓰는 클라이언트에서 테스트하세요. HTML 이메일에 이미지를 넣는 믿을 만한 방법은 cid:로 참조하는 인라인 MIME 첨부 파일이며, 이것도 Base64지만 MIME 구조 안에 있습니다. 이메일 속 Base64를 참고하세요.
PNG, JPEG, WebP, SVG를 끌어다 놓으면 data URI와 그 크기를 받을 수 있습니다. 파일은 브라우저 안에 머뭅니다.
자주 묻는 질문
Base64 이미지는 일반 이미지보다 느린가요?
본질적으로 그렇지는 않습니다. 작은 인라인 이미지는 요청을 줄이지만, 큰 이미지는 HTML이나 CSS를 부풀리고, 따로 캐시될 수 없으며, 렌더링을 늦출 수 있습니다. 몇 킬로바이트 이하에서는 인라인이 도움이 되고 그 이상에서는 손해입니다.
gzip이 Base64의 33% 오버헤드를 없애 주나요?
PNG, JPEG, WOFF2라면 거의 다 없애 줍니다. 저희 측정에서 gzip으로 압축한 Base64는 바이너리 파일과 몇 퍼센트 이내로 차이가 났습니다. SVG와 다른 텍스트는 그렇지 않아서, gzip으로 압축한 Base64 SVG는 gzip으로 압축한 SVG보다 41–47% 컸습니다.
인라인 SVG에는 URL 인코딩과 Base64 중 무엇이 좋은가요?
URL 인코딩입니다. 읽을 수 있고, 편집할 수 있으며, 원래 SVG와 거의 비슷하게 압축됩니다. 최소한 #, %, <, >는 이스케이프하고, 속성이 큰따옴표를 쓴다면 SVG 안에서는 작은따옴표를 쓰세요.
data URI 이미지가 왜 콘텐츠 보안 정책(CSP)에 막히나요?
CSP는 data:를 'self'에 포함되지 않는 별도의 스킴으로 취급합니다. img-src(글꼴은 font-src)에 data:를 추가하세요. script-src나 default-src에는 추가하지 마세요.