포함된 이미지 미리보기로 더 빠른 이미지 로드

게시 됨: 2022-03-10
빠른 요약 ↬ 이 기사에서 소개된 EIP(Embedded Image Preview) 기술을 사용하면 추가 데이터를 전송하지 않고도 점진적 JPEG, Ajax 및 HTTP 범위 요청을 사용하여 지연 로드 중에 미리보기 이미지를 로드할 수 있습니다.

LQIP(Low Quality Image Preview) 및 SVG 기반 변형 SQIP는 지연 이미지 로드를 위한 두 가지 주요 기술입니다. 둘 다 공통점은 먼저 낮은 품질의 미리 보기 이미지를 생성한다는 것입니다. 이것은 흐리게 표시되고 나중에 원본 이미지로 대체됩니다. 추가 데이터를 로드하지 않고도 웹사이트 방문자에게 미리보기 이미지를 제공할 수 있다면 어떨까요?

지연 로딩이 주로 사용되는 JPEG 파일은 사양에 따라 먼저 대략적인 이미지 내용이 표시되고 다음으로 자세한 이미지 내용이 표시되는 방식으로 파일에 포함된 데이터를 저장할 수 있습니다. 로드하는 동안 이미지가 위에서 아래로 구성되는 대신(기준 모드), 흐릿한 이미지가 매우 빠르게 표시될 수 있으며, 이는 점차 더 선명해집니다(프로그레시브 모드).

베이스라인 모드에서 JPEG의 시간적 구조 표현
베이스라인 모드(큰 미리보기)
프로그레시브 모드에서 JPEG의 시간 구조 표현
프로그레시브 모드(큰 미리보기)

더 빠르게 표시되는 모양으로 제공되는 더 나은 사용자 경험 외에도 프로그레시브 JPEG는 일반적으로 기준선으로 인코딩된 JPEG보다 작습니다. Yahoo 개발 팀의 Stoyan Stefanov에 따르면 10kB보다 큰 파일의 경우 프로그레시브 모드를 사용할 때 이미지가 더 작아질 확률이 94%입니다.

웹 사이트가 많은 JPEG로 구성된 경우 점진적 JPEG도 차례로 로드되는 것을 알 수 있습니다. 이는 최신 브라우저가 도메인에 대한 동시 연결을 6개만 허용하기 때문입니다. 따라서 프로그레시브 JPEG만으로는 사용자에게 페이지에 대한 가장 빠른 인상을 주는 솔루션이 아닙니다. 최악의 경우 브라우저는 다음 이미지 로드를 시작하기 전에 이미지를 완전히 로드합니다.

여기에 제시된 아이디어는 이제 서버에서 너무 많은 프로그레시브 JPEG 바이트만 로드하여 이미지 콘텐츠의 느낌을 빠르게 얻을 수 있다는 것입니다. 나중에 우리가 정의한 시간에(예: 현재 뷰포트의 모든 미리보기 이미지가 로드되었을 때), 미리보기를 위해 이미 요청한 부분을 다시 요청하지 않고 나머지 이미지를 로드해야 합니다.

EIP(Embedded Image Preview) 기술이 두 요청에서 이미지 데이터를 로드하는 방식을 보여줍니다.
두 개의 요청으로 프로그레시브 JPEG 로드(큰 미리보기)

불행히도 이미지의 어느 정도를 언제 로드해야 하는지 속성의 img 태그에 말할 수 없습니다. 그러나 Ajax에서는 이미지를 전달하는 서버가 HTTP 범위 요청을 지원한다면 이것이 가능합니다.

HTTP 범위 요청을 사용하여 클라이언트는 HTTP 응답에 포함되어야 하는 요청된 파일의 바이트를 HTTP 요청 헤더에서 서버에 알릴 수 있습니다. 각각의 대형 서버(Apache, IIS, nginx)에서 지원하는 이 기능은 주로 비디오 재생에 사용됩니다. 사용자가 비디오의 끝으로 점프하는 경우 사용자가 원하는 부분을 최종적으로 보기 전에 전체 비디오를 로드하는 것은 그다지 효율적이지 않습니다. 따라서 서버는 사용자가 요청한 시간 주변의 비디오 데이터만 요청하므로 사용자는 최대한 빨리 비디오를 시청할 수 있습니다.

우리는 이제 다음 세 가지 과제에 직면해 있습니다.

  1. 프로그레시브 JPEG 만들기
  2. 첫 번째 HTTP 범위 요청이 미리 보기 이미지를 로드해야 하는 최대 바이트 오프셋 결정
  3. 프론트엔드 자바스크립트 코드 생성
점프 후 더! 아래에서 계속 읽기 ↓

1. 프로그레시브 JPEG 만들기

프로그레시브 JPEG는 몇 가지 소위 스캔 세그먼트로 구성되며 각 세그먼트에는 최종 이미지의 일부가 포함됩니다. 첫 번째 스캔은 이미지를 아주 대략적으로만 보여주지만 파일의 뒷부분에 있는 스캔은 이미 로드된 데이터에 점점 더 자세한 정보를 추가하고 최종적으로 최종 모양을 형성합니다.

개별 스캔이 정확히 어떻게 보이는지는 JPEG를 생성하는 프로그램에 의해 결정됩니다. mozjpeg 프로젝트의 cjpeg 와 같은 명령줄 프로그램에서는 이러한 스캔에 포함된 데이터를 정의할 수도 있습니다. 그러나 이를 위해서는 이 기사의 범위를 벗어나는 보다 심층적인 지식이 필요합니다. 이를 위해 JPEG 압축의 기초를 가르치는 내 기사 "Finally Understanding JPG"를 참조하고 싶습니다. 스캔 스크립트에서 프로그램에 전달해야 하는 정확한 매개변수는 mozjpeg 프로젝트의 Wizard.txt에 설명되어 있습니다. 내 생각에 mozjpeg에서 기본적으로 사용하는 스캔 스크립트의 매개변수(7개 스캔)는 빠른 프로그레시브 구조와 파일 크기 사이의 좋은 절충안이므로 채택할 수 있습니다.

초기 JPEG를 프로그레시브 JPEG로 변환하기 위해 mozjpeg 프로젝트의 jpegtran 을 사용합니다. 기존 JPEG를 무손실로 변경하는 도구입니다. Windows 및 Linux용 사전 컴파일된 빌드는 https://mozjpeg.codelove.de/binaries.html에서 사용할 수 있습니다. 보안상의 이유로 안전하게 플레이하고 싶다면 직접 구축하는 것이 좋습니다.

명령줄에서 이제 점진적 JPEG를 만듭니다.

 $ jpegtran input.jpg > progressive.jpg

우리가 프로그레시브 JPEG를 만들고 싶다는 사실은 jpegtran에서 가정하며 명시적으로 지정할 필요가 없습니다. 이미지 데이터는 어떠한 경우에도 변경되지 않습니다. 파일 내 이미지 데이터의 배열만 변경됩니다.

Exif, IPTC 또는 XMP 데이터와 같은 이미지의 모양과 관련이 없는 메타데이터는 JPEG에서 제거하는 것이 좋습니다. 해당 세그먼트가 이미지 콘텐츠 앞에 있는 경우 메타데이터 디코더에서만 읽을 수 있기 때문입니다. 이러한 이유로 파일의 이미지 데이터 뒤로 이동할 수 없으므로 미리 보기 이미지와 함께 이미 전달되고 그에 따라 첫 번째 요청을 확대합니다. 명령줄 프로그램 exiftool 을 사용하면 다음 메타데이터를 쉽게 제거할 수 있습니다.

 $ exiftool -all= progressive.jpg

명령줄 도구를 사용하지 않으려면 온라인 압축 서비스 compress-or-die.com을 사용하여 메타데이터 없이 프로그레시브 JPEG를 생성할 수도 있습니다.

2. 첫 번째 HTTP 범위 요청이 미리 보기 이미지를 로드해야 하는 최대 바이트 오프셋 결정

JPEG 파일은 각각 다른 구성 요소(이미지 데이터, IPTC, Exif 및 XMP와 같은 메타데이터, 내장된 색상 프로필, 양자화 테이블 등)를 포함하는 여러 세그먼트로 나뉩니다. 이러한 각 세그먼트는 16진수 FF 바이트로 시작되는 마커로 시작합니다. 그 다음에는 세그먼트 유형을 나타내는 바이트가 옵니다. 예를 들어 D8 은 각 JPEG 파일이 시작되는 SOI 마커 FF D8 (이미지 시작)에 대한 마커를 완성합니다.

스캔의 각 시작은 SOS 마커(스캔 시작, 16진수 FF DA )로 표시됩니다. SOS 마커 뒤에 있는 데이터는 엔트로피 코딩되어 있기 때문에(JPEG는 Huffman 코딩을 사용함) SOS 세그먼트 이전에 디코딩에 필요한 Huffman 테이블(DHT, 16진수 FF C4 )이 있는 또 다른 세그먼트가 있습니다. 따라서 프로그레시브 JPEG 파일 내에서 우리의 관심 영역은 Huffman 테이블/스캔 데이터 세그먼트가 번갈아 가며 구성됩니다. 따라서 이미지의 첫 번째 대략적인 스캔을 표시하려면 서버에서 DHT 세그먼트(16진수 FF C4 )의 두 번째 발생까지 모든 바이트를 요청해야 합니다.

JPEG 파일에 SOS 마커를 표시합니다.
JPEG 파일의 구조(큰 미리보기)

PHP에서 다음 코드를 사용하여 모든 스캔에 필요한 바이트 수를 배열로 읽을 수 있습니다.

 <?php $img = "progressive.jpg"; $jpgdata = file_get_contents($img); $positions = []; $offset = 0; while ($pos = strpos($jpgdata, "\xFF\xC4", $offset)) { $positions[] = $pos+2; $offset = $pos+2; }

브라우저는 새로운 마커(방금 언급한 것처럼 2바이트로 구성)를 만날 때만 미리보기 이미지의 마지막 행을 렌더링하기 때문에 발견된 위치에 2라는 값을 추가해야 합니다.

이 예제의 첫 번째 미리보기 이미지에 관심이 있으므로 HTTP Range Request를 통해 파일을 요청해야 하는 $positions[1] 의 올바른 위치를 찾습니다. 더 나은 해상도의 이미지를 요청하려면 배열의 나중 위치를 사용할 수 있습니다(예: $positions[3] .

3. 프론트엔드 자바스크립트 코드 생성

우선, 방금 평가된 바이트 위치를 제공하는 img 태그를 정의합니다.

 <img data-src="progressive.jpg" data-bytes="<?= $positions[1] ?>">

지연 로드 라이브러리의 경우와 같이 HTML 코드를 구문 분석할 때 브라우저가 즉시 서버에서 이미지 요청을 시작하지 않도록 src 속성을 직접 정의하지 않습니다.

다음 JavaScript 코드를 사용하여 이제 미리보기 이미지를 로드합니다.

 var $img = document.querySelector("img[data-src]"); var URL = window.URL || window.webkitURL; var xhr = new XMLHttpRequest(); xhr.onload = function(){ if (this.status === 206){ $img.src_part = this.response; $img.src = URL.createObjectURL(this.response); } } xhr.open('GET', $img.getAttribute('data-src')); xhr.setRequestHeader("Range", "bytes=0-" + $img.getAttribute('data-bytes')); xhr.responseType = 'blob'; xhr.send();

이 코드는 HTTP 범위 헤더의 서버에 파일을 처음부터 data-bytes 에 지정된 위치까지 반환하도록 지시하는 Ajax 요청을 생성합니다. 서버가 HTTP 범위 요청을 이해하면 이진 이미지 데이터를 HTTP-206 응답(HTTP 206 = 부분 콘텐츠)으로 반환하며, 여기에서 createObjectURL 을 사용하여 브라우저 내부 URL을 생성할 수 있습니다. 이 URL을 img 태그의 src 로 사용합니다. 따라서 미리보기 이미지를 로드했습니다.

이 데이터가 즉시 필요하기 때문에 src_part 속성의 DOM 개체에 blob을 추가로 저장합니다.

개발자 콘솔의 네트워크 탭에서 전체 이미지가 아니라 작은 부분만 로드했는지 확인할 수 있습니다. 또한 Blob URL의 로드는 0바이트 크기로 표시되어야 합니다.

네트워크 콘솔과 HTTP 요청의 크기를 보여줍니다.
미리보기 이미지를 로드할 때 네트워크 콘솔(큰 미리보기)

원본 파일의 JPEG 헤더를 이미 로드했기 때문에 미리보기 이미지의 크기가 정확합니다. 따라서 애플리케이션에 따라 img 태그의 높이와 너비를 생략할 수 있습니다.

대안: 미리보기 이미지 인라인 로드

성능상의 이유로 미리보기 이미지의 데이터를 HTML 소스 코드에서 직접 데이터 URI로 전송할 수도 있습니다. 이렇게 하면 HTTP 헤더를 전송하는 오버헤드가 줄어들지만 base64 인코딩은 이미지 데이터를 1/3 더 크게 만듭니다. 이것은 gzip 또는 brotli 와 같은 콘텐츠 인코딩으로 HTML 코드를 제공하는 경우 상대적이지만 작은 미리보기 이미지에는 여전히 데이터 URI를 사용해야 합니다.

훨씬 더 중요한 사실은 미리보기 이미지를 즉시 사용할 수 있고 페이지를 구축할 때 사용자에게 눈에 띄는 지연이 없다는 것입니다.

우선, 우리는 데이터 URI를 생성해야 합니다. 그런 다음 img 태그에서 src 로 사용합니다. 이를 위해 PHP를 통해 데이터 URI를 생성합니다. 이 코드는 SOS 마커의 바이트 오프셋을 결정하는 방금 생성된 코드를 기반으로 합니다.

 <?php … $fp = fopen($img, 'r'); $data_uri = 'data:image/jpeg;base64,'. base64_encode(fread($fp, $positions[1])); fclose($fp);

생성된 데이터 URI는 이제 `img` 태그에 src 로 직접 삽입됩니다.

 <img src="<?= $data_uri ?>" data-src="progressive.jpg" alt="">

물론 JavaScript 코드도 수정해야 합니다.

 <script> var $img = document.querySelector("img[data-src]"); var binary = atob($img.src.slice(23)); var n = binary.length; var view = new Uint8Array(n); while(n--) { view[n] = binary.charCodeAt(n); } $img.src_part = new Blob([view], { type: 'image/jpeg' }); $img.setAttribute('data-bytes', $img.src_part.size - 1); </script>

즉시 Blob을 수신하는 Ajax 요청을 통해 데이터를 요청하는 대신 이 경우 데이터 URI에서 직접 Blob을 만들어야 합니다. 이를 위해 이미지 데이터가 포함되지 않은 부분에서 data-URI를 해제합니다: data:image/jpeg;base64 . 나머지 base64 코딩 데이터를 atob 명령으로 디코딩합니다. 이제 이진 문자열 데이터에서 blob을 만들려면 데이터를 Uint8 배열로 전송해야 합니다. 그러면 데이터가 UTF-8로 인코딩된 텍스트로 처리되지 않습니다. 이 배열에서 이제 미리보기 이미지의 이미지 데이터로 이진 블롭을 만들 수 있습니다.

이 인라인 버전에 대해 다음 코드를 적용할 필요가 없도록 img 태그에 data-bytes 속성을 추가합니다. 이 태그에는 이전 예에서 이미지의 두 번째 부분을 로드해야 하는 바이트 오프셋이 포함되어 있습니다. .

개발자 콘솔의 네트워크 탭에서 미리보기 이미지를 로드해도 추가 요청이 발생하지 않는 반면 HTML 페이지의 파일 크기가 증가한 것을 여기서 확인할 수도 있습니다.

네트워크 콘솔과 HTTP 요청의 크기를 보여줍니다.
미리보기 이미지를 데이터 URI로 로드할 때 네트워크 콘솔(큰 미리보기)

최종 이미지 로드

두 번째 단계에서는 예를 들어 2초 후에 나머지 이미지 파일을 로드합니다.

 setTimeout(function(){ var xhr = new XMLHttpRequest(); xhr.onload = function(){ if (this.status === 206){ var blob = new Blob([$img.src_part, this.response], { type: 'image/jpeg'} ); $img.src = URL.createObjectURL(blob); } } xhr.open('GET', $img.getAttribute('data-src')); xhr.setRequestHeader("Range", "bytes="+ (parseInt($img.getAttribute('data-bytes'), 10)+1) +'-'); xhr.responseType = 'blob'; xhr.send(); }, 2000);

Range 헤더에서 이번에는 미리보기 이미지의 끝 위치에서 파일 끝까지 이미지를 요청하도록 지정합니다. 첫 번째 요청에 대한 응답은 DOM 객체의 src_part 속성에 저장됩니다. 두 요청의 응답을 사용하여 전체 이미지의 데이터를 포함하는 new Blob() 마다 새 blob을 만듭니다. 여기서 생성된 blob URL은 DOM 개체의 src 로 다시 사용됩니다. 이제 이미지가 완전히 로드되었습니다.

또한 이제 개발자 콘솔의 네트워크 탭에서 로드된 크기를 다시 확인할 수 있습니다.

네트워크 콘솔과 HTTP 요청의 크기를 보여줍니다.
전체 이미지 로드 시 네트워크 콘솔(31.7kB)(큰 미리보기)

원기

다음 URL에서 다양한 매개변수를 실험할 수 있는 프로토타입을 제공했습니다. https://embedded-image-preview.cerdmann.com/prototype/

프로토타입에 대한 GitHub 리포지토리는 https://github.com/McSodbrenner/embedded-image-preview에서 찾을 수 있습니다.

마지막 고려 사항

여기에 제시된 EIP(Embedded Image Preview) 기술을 사용하여 Ajax 및 HTTP 범위 요청을 통해 프로그레시브 JPEG에서 질적으로 다른 미리보기 이미지를 로드할 수 있습니다. 이러한 미리보기 이미지의 데이터는 삭제되지 않고 대신 전체 이미지를 표시하는 데 재사용됩니다.

또한 미리보기 이미지를 만들 필요가 없습니다. 서버 측에서는 미리보기 이미지가 끝나는 바이트 오프셋만 결정하고 저장해야 합니다. CMS 시스템에서는 이 숫자를 이미지의 속성으로 저장할 수 있어야 하며 img 태그에서 출력할 때 이를 고려해야 합니다. 그림 파일과 오프셋을 저장하지 않아도 되도록 그림의 파일 이름을 오프셋(예: progress-8343.jpg )으로 보완하는 워크플로도 생각할 수 있습니다. 이 오프셋은 JavaScript 코드로 추출할 수 있습니다.

미리보기 이미지 데이터가 재사용되기 때문에 이 기술은 미리보기 이미지를 로드한 다음 WebP를 로드하는 일반적인 접근 방식에 대한 더 나은 대안이 될 수 있습니다(WebP를 지원하지 않는 브라우저에 JPEG 폴백 제공). 미리보기 이미지는 종종 프로그레시브 모드를 지원하지 않는 WebP의 저장 이점을 파괴합니다.

현재 일반 LQIP의 미리보기 이미지는 미리보기 데이터를 로드하는 데 추가 대역폭이 필요하다고 가정하기 때문에 품질이 떨어집니다. Robin Osborne이 2018년 블로그 게시물에서 이미 분명히 밝혔듯이 최종 이미지에 대한 아이디어를 제공하지 않는 자리 표시자를 표시하는 것은 의미가 없습니다. 여기에 제안된 기술을 사용하여 사용자에게 프로그레시브 JPEG의 나중 스캔을 제시함으로써 망설임 없이 미리보기 이미지로 최종 이미지의 일부를 더 표시할 수 있습니다.

사용자의 네트워크 연결이 약한 경우 응용 프로그램에 따라 전체 JPEG를 로드하지 않고 예를 들어 마지막 두 스캔을 생략하는 것이 합리적일 수 있습니다. 이렇게 하면 품질이 약간 저하된 훨씬 작은 JPEG가 생성됩니다. 사용자는 그것에 대해 감사할 것이고 우리는 서버에 추가 파일을 저장할 필요가 없습니다.

이제 프로토타입을 시험해 보는 데 많은 재미를 느끼길 바라며 여러분의 의견을 기다립니다.