이미지 압축 벤치마크: 브라우저에서 측정한 열 가지 샘플
열 가지 샘플, 각각 다섯 번 측정, 브라우저 하나. 사이트가 실제로 배포하는 워커 경로에서 잰 인코더 측정값이며, 결과가 우리에게 유리하지 않은 경우도 포함합니다.
무엇을 어떻게 측정했나?
일곱 개 파일에서 나온 열 가지 경우의 압축 시간과 출력 크기를, 브라우저 하나와 기기 하나에서 각각 다섯 번 측정합니다. 12 MP 사진은 JPEG, WebP, AVIF로 인코딩하고, 2 MP 스크린샷은 PNG와 WebP로 인코딩합니다.
측정 도구는 실제 배포 경로를 그대로 씁니다. src/lib/pool.js와 src/lib/worker.js를 가져옵니다. 앱이 JPEG, PNG, WebP, AVIF, JXL에 쓰는 것과 같은 풀, 같은 워커입니다. MotionPhoto는 src/lib/motion-photo/pipeline.js를 가져오며, 앱이 그 형식에 쓰는 경로가 바로 이것입니다. MotionPhoto 역시 프로덕션에서 워커 풀을 거치지 않습니다. 벤치마크 전용 압축기도, 따로 어긋날 두 번째 구현도 없습니다.
각 경우는 예열 한 번, 그다음 측정 다섯 번을 실행합니다. 경우는 순차적으로 실행되어 네 개 워커 풀을 두고 경쟁하지 않습니다. 표의 시간은 다섯 샘플의 중앙값입니다. p95는 최근접 순위로 계산하므로 다섯 샘플에서는 가장 느린 실행입니다. 보간하지 않고, 다섯 샘플이 꼬리를 설명한다고 가장하지도 않습니다.
브라우저는 헤드리스 Chrome 155이고 실행마다 새 임시 프로필을 씁니다. 확장과 백그라운드 네트워크는 꺼져 있습니다. localhost에서 제공하며, 워커 풀과 WASM 모듈이 준비된 뒤에 타이머를 시작합니다. 교차 출처 격리는 일부러 켜지 않습니다. 켜면 wasm-feature-detect가 스레드를 보고하고 oxipng가 스레드 빌드로 바뀌는데, 프로덕션이 쓰는 경로가 아닙니다.
기기는 Apple M4, 10코어, 16GB 메모리, macOS arm64입니다. 브라우저는 하드웨어 스레드 10개를 보고하고, 앱은 풀을 네 워커로 제한합니다.
어떤 파일을 테스트했나?
0.5 MP JPEG부터 24 MP 사진까지 일곱 개 파일에, 12 MP MotionPhoto 하나를 더했습니다.
| 파일 | 픽셀 | 입력 | 무엇을 나타내나 |
|---|---|---|---|
| photo-12mp.jpg | 4000×3000 | 4.92 MB | 디테일이 많은 사진 |
| shot-2mp.png | 1920×1080 | 1.36 MB | 날카로운 가장자리와 텍스트 모양 막대가 있는 UI 스크린샷 |
| flat-1mp.png | 1000×1000 | 118.1 KB | 평평하고 디테일이 적은 그래픽 |
| alpha-4mp.png | 2000×2000 | 8.26 MB | 실제 알파 채널이 있는 RGBA 이미지 |
| large-24mp.jpg | 6000×4000 | 9.66 MB | 24 MP 사진 |
| tiny-0.5mp.jpg | 800×600 | 219.6 KB | 작은 이미지. 호출당 비용이 여기서 드러남 |
| motion-12mp.jpg | 4000×3000 정지 프레임 + 6초 1280×720 동영상 | 9.98 MB | 저장소의 기록기로 조립한 합성 MotionPhoto |
모든 샘플은 ffmpeg가 내장 소스로 생성합니다. 각 파일의 SHA-256은 bench/fixtures/manifest.json에 기록되고 브라우저가 로드하기 전에 검증됩니다. 따라서 샘플이 바뀌면 조용히 기준선이 이동하는 것이 아니라 실패합니다.
결과는 어떤가?
열 가지 경우의 비율은 1.5%에서 69.3%, 중앙값은 42.0 ms에서 1,983.2 ms입니다. 비율은 출력 ÷ 입력이며 낮을수록 작습니다. 중앙값과 p95는 워커 왕복을 포함한 전체 경우의 실제 시간입니다. ms/MP는 중앙값을 메가픽셀 수로 나눈 값입니다.
표와 두 차트는 같은 bench/published.json(실행 ID, 날짜, 환경)을 읽습니다. 다르면 파일이 기준입니다.
| 경우 | 형식 | MP | 입력 | 출력 | 비율 | 중앙값 | p95 | ms/MP |
|---|---|---|---|---|---|---|---|---|
| photo-12mp-jpg | JPEG → JPEG | 12 | 4.92 MB | 444.4 KB | 8.8% | 677.0 | 733.4 | 56.4 |
| photo-12mp-webp | JPEG → WebP | 12 | 4.92 MB | 325.2 KB | 6.5% | 997.5 | 1021.7 | 83.1 |
| photo-12mp-avif | JPEG → AVIF | 12 | 4.92 MB | 121.8 KB | 2.4% | 1983.2 | 2026.8 | 165.3 |
| shot-2mp-png | PNG → PNG | 2.07 | 1.36 MB | 600.6 KB | 43.2% | 613.1 | 618.1 | 296.2 |
| shot-2mp-webp | PNG → WebP | 2.07 | 1.36 MB | 21.0 KB | 1.5% | 151.3 | 156.1 | 73.1 |
| flat-1mp-png | PNG → PNG | 1 | 118.1 KB | 61.0 KB | 51.7% | 516.2 | 562.9 | 516.2 |
| alpha-4mp-avif | PNG → AVIF | 4 | 8.26 MB | 370.1 KB | 4.4% | 1551.7 | 1626.3 | 387.9 |
| large-24mp-jpg | JPEG → JPEG | 24 | 9.66 MB | 846.1 KB | 8.6% | 1228.1 | 1231.0 | 51.2 |
| tiny-0.5mp-webp | JPEG → WebP | 0.48 | 219.6 KB | 20.4 KB | 9.3% | 42.0 | 43.4 | 87.5 |
| motion-12mp | MotionPhoto | 12 | 9.98 MB | 6.92 MB | 69.3% | 1334.0 | 1377.3 | 111.2 |
숫자가 말하는 것은?
12 MP 사진에서 AVIF는 JPEG보다 3.65배 작고 2.93배 느리며, WebP는 그 사이, PNG는 296.2 ms/MP의 무손실 예외입니다.
AVIF는 JPEG보다 얼마나 작은가?
JPEG의 약 3분의 1입니다. 12 MP 사진에서 2.4% 대 8.8%, 즉 121.8 KB 대 444.4 KB입니다. WebP는 325.2 KB였습니다. AVIF는 JPEG보다 3.65배 작고 WebP보다 2.67배 작습니다. 가장 느리기도 합니다. 중앙값 1,983.2 ms로 JPEG 시간의 2.93배, WebP의 1.99배입니다. 메가픽셀당 165.3 ms이며 JPEG는 56.4, WebP는 83.1입니다.
한 줄로 요약하면 JPEG 바이트의 4분의 1을 조금 넘는 크기로 인코딩 시간이 세 배입니다. 수백 장 배치에서는 이 시간이 실제이고, 대표 이미지 한 장에서는 중요하지 않을 수 있습니다.
WebP는 JPEG보다 얼마나 작고 느린가?
26.8% 작고 1.47배 느립니다. 같은 12 MP 원본에서 325.2 KB 대 444.4 KB, 997.5 ms 대 677.0 ms입니다. AVIF와 비교하면 2.67배 크고 1.99배 빠릅니다. 어느 쪽 끝에서도 최고가 아니며, 양쪽 방향의 중간입니다.
JPEG는 얼마나 빠른가?
12 MP 사진 677.0 ms(56.4 ms/MP), 24 MP 사진 1,228.1 ms(51.2 ms/MP)로 여기 세 코덱 중 가장 빠릅니다. 메가픽셀당 비용이 거의 평평한데, 빠른 블록 코덱의 모습입니다. 출력은 두 현대 형식보다 크지만 시간에서 이겨야 할 기준이고, 이 기기에서는 아무도 이기지 못했습니다.
PNG는 왜 메가픽셀당 가장 느린가?
2.07 MP 스크린샷에서 296.2 ms/MP로 WebP의 73.1 ms/MP보다 4.05배 느립니다. 무손실 최적화가 모든 픽셀에 대해 필터와 DEFLATE 전략을 탐색해야 하기 때문입니다. 같은 스크린샷은 PNG로 613.1 ms, WebP로 151.3 ms였습니다. 평평한 1 MP PNG는 픽셀당 더 나빴습니다. 516.2 ms인데, 최적화기의 고정 비용이 1 메가픽셀에만 분산되기 때문입니다.
PNG는 무손실입니다. 모든 픽셀을 보존하며, oxipng는 더 작은 스트림을 찾아 필터와 DEFLATE 전략을 탐색하는 데 시간을 씁니다. 탐색이 목적이자 비용입니다. 손실 코덱은 품질 목표에 도달하면 멈출 수 있습니다. 무손실 최적화기는 더 제거할 것이 없다는 것을 입증해야 하고, 조합 수는 이미지와 함께 늘어납니다.
크기 차이도 단호합니다. 같은 스크린샷이 PNG로 600.6 KB, WebP로 21.0 KB였습니다. 두 출력은 동등하지 않습니다. 하나는 무손실이고 하나는 손실입니다. 하지만 왜 PNG가 편집할 다이어그램에는 맞고 배포할 사진에는 틀린지 설명합니다.
oxipng 레벨이 높으면 PNG가 항상 작아지나?
아닙니다. 레벨 3은 스크린샷 세 개에서 9.6–13.6% 작았지만, 8.29 MP 사진형 PNG에서는 8.2배 시간에 겨우 4바이트를 줄였습니다. PNG 레벨은 bench/png-levels.mjs로 따로 측정했습니다. 같은 샘플에 레벨마다 예열 한 번과 다섯 번을 실행했습니다. 스크린샷 샘플 세 개(2.07, 8.29, 15.73 MP)에서 레벨 3은 레벨 1보다 7.8–8.5배 오래 걸리고 파일은 9.6–13.6% 작았습니다. 8.29 MP 사진형 PNG에서는 레벨 1보다 겨우 4바이트 작았고(15,474,918 대 15,474,922) 시간은 8.2배였습니다. RGBA 이미지에서는 2.0%를 아끼고 7.1배 시간이 걸렸습니다. 이미 사진인 이미지에서는 추가 탐색이 거의 아무것도 사지 못합니다.
그래서 배포 워커는 이제 기본으로 레벨 2를 쓰고 4 MP를 넘으면 레벨 1로 내립니다. 16 MP에서 레벨 2는 레벨 1보다 1.0% 크고 약 두 배 느렸으므로, 높은 레벨이 자동으로 더 작은 파일은 아닙니다. 변경 비용은 평평하고 디테일이 적은 PNG에서 보입니다. 1 MP 평평한 샘플은 레벨 2에서 62,501 B, 레벨 3에서 48,138 B로 29.8% 크지만, 인코딩은 437.9 ms로 2,028.3 ms보다 4.6배 빠릅니다. 스크린샷은 덜 냅니다. 2.07 MP에서 레벨 2 파일은 레벨 3보다 7.2% 크고 4.1배 빠릅니다. 사이트는 더 빠른 기본값을 택하고 그 바이트를 받아들였습니다.
MotionPhoto는 동영상도 압축하나?
아닙니다. 정지 프레임만 다시 인코딩하고 동영상은 바이트 단위로 복사하므로, 합성 12 MP 파일은 절반을 건드리지 않은 채 30.7% 줄었습니다. MotionPhoto는 파일 하나입니다. JPEG 정지 프레임에 동영상이 붙고, 일부 기기에는 gain map도 붙습니다. 배포 경로는 정지 프레임을 디코딩하고 다시 인코딩한 뒤 그 주위로 컨테이너를 재구성합니다. 디코딩하지 않고, 다시 인코딩하지 않고, 대상으로 제공하지도 않습니다. gain map이 있으면 이것도 복사하고 MPF의 절대 오프셋을 다시 지정해 HDR 표시가 계속 동작하게 합니다.
따라서 파일 전체 비율은 동영상이 차지하는 몫에 좌우됩니다. 합성 12 MP 샘플에서 정지 프레임은 입력 10,463,084 B 중 5,156,418 B(49.3%)이고 동영상은 5,306,666 B(50.7%)입니다. gain map은 없습니다. 정지 프레임은 1,944,569 B로 62.3% 줄었고, 파일 전체는 9.98 MB에서 6.92 MB로 30.7% 줄었습니다. 정지 프레임 자체의 절감은 크지만, 파일 절감은 손대지 않은 절반 때문에 제한됩니다.
실제 샘플 검증은 OnePlus 13 MotionPhoto를 씁니다. 정지 프레임 7,140,978 B(44.7%), 동영상 7,968,544 B(49.9%), gain map 848,042 B(5.3%), 합계 15,957,564 B입니다. 정지 프레임만 처리한 뒤: 정지 프레임 1,862,140 B(73.9% 감소), 동영상과 gain map은 바이트 단위로 동일, 합계 10,712,466 B — 15.96 MB에서 10.71 MB로 32.9% 감소. 재구성된 파일은 휴대폰에서 MotionPhoto로 인식되고 재생되었습니다. gain map 경로를 실제 하드웨어에서 검증한 것이며, 합성 샘플에서 추론한 것이 아닙니다.
한계는 무엇인가?
합성 샘플과 기기 한 대라는 점에서 시작하는 여섯 가지 단서입니다.
- 샘플은 ffmpeg가 내장 소스로 생성합니다. 재현 가능하지만 카메라 사진은 아닙니다. 디테일이 다른 이미지는 다른 비율을 냅니다.
- MotionPhoto 샘플은 합성이며 gain map이 없습니다. 파일 전체 비율 69.3%는 실제 샘플의 67.1%와 다릅니다. 차이는 빠진 gain map과 정확한 동영상 비트레이트에서 옵니다. 실제 샘플은 따로 보고합니다.
- 이 숫자는 한 기기, 한 브라우저, 한 풀 크기에서 나왔습니다. 다른 하드웨어에서는 절대 시간이 달라지고, 모바일에서는 더 달라집니다.
- 숫자는 같은 환경 안에서만 비교할 수 있습니다. 벤치마크 출력도 그렇게 말합니다. 로컬 darwin-arm64와 CI ubuntu-latest는 서로 다른 runner 등급입니다.
- 이는 인코더 측정이며 특정 휴대폰에 대한 약속이 아닙니다. 배터리 상태, 열 스로틀링, 브라우저 버전 차이, 결과 디코딩 비용은 포함하지 않습니다.
- 최대 메모리는 보고하지 않습니다. 측정하려면 교차 출처 격리가 필요하고 그것이 oxipng 경로를 바꿉니다. 벤치마크는 다른 빌드의 숫자 대신 "n/a"를 적습니다.
이 숫자를 어떻게 재현하나?
npm ci부터 npm run bench까지 다섯 개 명령입니다.
npm ci
npm run build
npm run bench:fixtures
npm run bench:build
npm run bench
실행은 bench/results/에 JSON을 쓰고 위 표를 stdout에 출력합니다. PNG 레벨 비교는 npm run bench:png-levels입니다. 샘플은 브라우저가 로드하기 전에 bench/fixtures/manifest.json과 대조합니다. 파일이 바뀌면 조용한 새 기준선이 아니라 확실한 실패입니다.
자주 묻는 질문
어떤 형식이 가장 작게 압축하나?
AVIF입니다. 12 MP 사진에서 121.8 KB로 입력의 2.4%였고, WebP는 6.5%, JPEG는 8.8%였습니다.
어떤 형식이 가장 빠르게 인코딩하나?
JPEG입니다. 12 MP 사진에 677.0 ms가 걸렸고, WebP는 997.5 ms, AVIF는 1,983.2 ms였습니다.
PNG는 왜 메가픽셀당 더 느린가?
무손실이기 때문입니다. oxipng는 모든 픽셀에 대해 필터와 DEFLATE 전략을 탐색합니다. 2.07 MP 스크린샷은 PNG로 296.2 ms/MP, WebP로 73.1 ms/MP로 4.05배 적었습니다.
MotionPhoto는 동영상을 압축하나?
아닙니다. 동영상은 바이트 단위로 복사되고 정지 프레임만 다시 인코딩됩니다. 합성 12 MP 샘플에서 정지 프레임은 62.3%, 파일 전체는 30.7% 줄어 9.98 MB에서 6.92 MB가 되었습니다.
레벨 2와 레벨 3은 PNG 출력 크기에 어떤 영향을 주나?
레벨 3이 더 작지만 훨씬 느립니다. 1 MP 평평한 PNG는 레벨 2에서 62,501 B, 레벨 3에서 48,138 B로 29.8% 크지만, 인코딩은 437.9 ms로 2,028.3 ms보다 빠릅니다.
각 경우를 몇 번 측정하나?
예열 한 번과 측정 다섯 번입니다. 표는 그 다섯 번의 중앙값을 보여 주고, p95는 가장 느린 샘플입니다.
이 글에 대하여
tinyimg가 게시합니다. 측정 도구, 샘플 생성기, 결과 파일이 저장소에 있고 이 실행의 원시 JSON은 bench/results/에 기록됩니다. 서명은 개인이 아니라 프로젝트입니다. 여기서 주장하는 것은 권위가 아니라 방법이 기록되어 있고 숫자를 재현할 수 있다는 점입니다.