Просмотр изображения с низким качеством (L-IP) и вариант на основе SVG S’IP являются двумя преобладающими методами для ленивой загрузки изображений. Что объединяет обоих, так это то, что сначала создается низкокачественное изображение предварительного просмотра. Это будет отображаться размыто, а затем заменено исходным изображением. Что делать, если вы могли бы представить изображение предварительного просмотра для посетителя веб-сайта без необходимости загружать дополнительные данные?
Файлы JPEG, для которых в основном используется ленивая загрузка, имеют возможность, согласно спецификации, хранить данные, содержащиеся в них, таким образом, что сначала отображаются грубые, а затем подробное содержимое изображения. Вместо того, чтобы изображение построено сверху вниз во время загрузки (базовый режим), размытое изображение может отображаться очень быстро, что постепенно становится острее и острее (прогрессивный режим).
В дополнение к лучшему пользовательскому опыту, обеспечиваемому более быстрым внешним видом, прогрессивные JEGs, как правило, также меньше, чем их базовые закодированные аналоги. Для файлов размером более 10 кБ вероятность попадания изображения в меньшую размер изображения при использовании прогрессивного режима, по словам Стоянка Стефанова из команды разработчиков Yahoo, составляет 94 процента.
Если ваш веб-сайт состоит из многих JEGs, вы заметите, что даже прогрессивные JEGs загружаются один за другим. Это потому, что современные браузеры позволяют только шесть одновременных подключений к домену. Прогрессивные JEGs сами по себе, следовательно, не решение, чтобы дать пользователю максимально быстрое впечатление от страницы. В худшем случае браузер загрузит изображение полностью, прежде чем он начнет загрузку следующего.
Идея, представленная здесь, заключается в том, чтобы загрузить только так много байтов прогрессивного JPEG с сервера, что вы можете быстро получить представление о содержании изображения. Позже, в определенное нами время (например, когда все изображения предварительного просмотра в текущем поле зрения были загружены), остальная часть изображения должна быть загружена без запроса детали, уже запрошенной для предварительного просмотра снова.
К сожалению, вы не можете сказать img
тег в атрибуте, сколько изображения должно быть загружено в какое время. С Ajax, однако, это возможно, при условии, что сервер доставки изображения поддерживает HTTP Range Requests.
Используя запросы диапазона HTTP, клиент может сообщить серверу в заголовке запроса HTTP, который байты запрашиваемого файла должны содержаться в ответе HTTP. Эта функция, поддерживаемая каждым из больших серверов (Apache, IIS, nginx), в основном используется для воспроизведения видео. Если пользователь перепрыгивает в конец видео, было бы не очень эффективно загружать полное видео, прежде чем пользователь сможет, наконец, увидеть нужную часть. Таким образом, только видео данные вокруг времени, запрошенного пользователем запрашивается сервером, так что пользователь может смотреть видео как можно быстрее.
Теперь перед нами стоят следующие три задачи:
- Создание прогрессивного JPEG
- Определите Байт Смещение до которых первый запрос http диапазона должен загрузить изображение предварительного просмотра
- Создание фронтендного кода JavaScript
1. Создание прогрессивного JPEG
Прогрессивный JPEG состоит из нескольких так называемых сегментов сканирования, каждый из которых содержит часть конечного изображения. Первое сканирование показывает изображение только очень грубо, в то время как те, которые следуют позже в файле добавить больше и более подробную информацию к уже загруженных данных и, наконец, сформировать окончательный вид.
Как именно выглядит индивидуальное сканирование, определяетпрограмма, генерируемая JEGs. В командных программах, таких как cjpeg из проекта mozjpeg,можно даже определить, какие данные содержат эти сканы. Однако для этого требуются более глубокие знания, которые выходят за рамки настоящей статьи. Для этого я хотел бы сослаться на мою статью «Наконец Понимание JPG«, который учит основам сжатия JPEG. Точные параметры, которые должны быть переданы программе в скрипте сканирования, объясняются в wizard.txt проекта mozjpeg. На мой взгляд, параметры сканирующего скрипта (семь сканирований), используемые mozjpeg по умолчанию, являются хорошим компромиссом между быстрой прогрессивной структурой и размером файла и поэтому могут быть приняты.
Чтобы превратить наш первоначальный JPEG в прогрессивный JPEG, мы используем jpegtran
из проекта mozjpeg. Это инструмент для внесения безпотерьных изменений в существующий 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. Определите Байт Offset До которых первый запрос диапазона HTTP должен загружать изображение предварительного просмотра
Файл JPEG делится на различные сегменты, каждый из которых содержит различные компоненты (данные изображения, метаданные, такие как IPTC, Exif и XMP, встроенные цветовые профили, таблицы количественной оценки и т.д.). Каждый из этих сегментов начинается с маркера, введенного гексадецимальным FF
байтом. За этим следует байт с указанием типа сегмента. Например, D8
завершает маркер маркера SOI FF D8
(Start Of Image), с которого начинается каждый файл JPEG.
Каждый старт сканирования отмечен маркером SOS (Начало сканирования, гексадецимальный). FF DA
Так как данные, стоящие за маркером SOS, закодированы (JPEGs используют кодирование Хаффмана),есть еще один сегмент со таблицами Хаффмана (DHT, hexadecimal), FF C4
необходимых для расшифровки перед сегментом SOS. Таким образом, область интереса для нас в прогрессивном файле JPEG состоит из чередования таблиц Хаффмана/сканирования сегментов данных. Таким образом, если мы хотим отобразить первое очень грубое сканирование изображения, мы должны запросить все байты до второго появления сегмента DHT (гексадецимальный) FF C4
с сервера.
В PHP мы можем использовать следующий код для чтения количества байтов, необходимых для всех сканирований, в массиве:
<?php
$img = "progressive.jpg";
$jpgdata = file_get_contents($img);
$positions = [];
$offset = 0;
while ($pos = strpos($jpgdata, "xFFxC4", $offset)) {
$positions[] = $pos+2;
$offset = $pos+2;
}
Мы должны добавить значение двух к найденному положению, потому что браузер отображает только последнюю строку изображения предварительного просмотра, когда он сталкивается с новым маркером (который состоит из двух байтов, как только что упоминалось).
Так как мы заинтересованы в первом изображении предварительного просмотра в этом примере, мы находим правильное положение, в $positions[1]
котором мы должны запросить файл через HTTP Range Request. Чтобы запросить изображение с лучшим разрешением, мы могли бы использовать более позднюю позицию в массиве, например $positions[3]
.
3. Создание фронтендного Кода JavaScript
Прежде всего, мы определяем img
тег, к которому мы даем только оцененную позицию байта:
<img data-src="progressive.jpg" data-bytes="<?= $positions[1] ?>">
Как это часто бывает с ленивыми библиотеками загрузки, мы не определяем src
атрибут напрямую, чтобы браузер не сразу начал запрашивать изображение с сервера при разборе HTML-кода.
Со следующим кодом 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();
Этот код создает запрос Ajax, который говорит серверу в заголовке диапазона HTTP, чтобы вернуть файл от начала до позиции, указанной в data-bytes
… и не более того. Если сервер понимает ЗАПРОСы HTTP Range, он возвращает данные двоичного изображения в ответе HTTP-206 (HTTP 206 — Частичное содержание) в виде капли, из которой мы можем создать url-адрес createObjectURL
браузера. Мы используем этот URL как src
для нашего img
тега. Таким образом, мы загрузили наше изображение предварительного просмотра.
Мы храним каплю дополнительно на объекте DOM в src_part
собственности, потому что мы будем нуждаться в этих данных немедленно.
В сетевой вкладке консоли разработчика можно проверить, не загрузили ли мы полное изображение, а лишь небольшую часть. Кроме того, загрузка URL-адреса капли должна отображаться с размером 0 байтов.
Поскольку мы уже загружаем заголовок JPEG исходного файла, изображение предварительного просмотра имеет правильный размер. Таким образом, в зависимости от приложения, мы можем опустить высоту и ширину img
тега.
Альтернатива: Загрузка изображения предварительного просмотра в строке
По причинам производительности также возможно передавать данные изображения предварительного просмотра в качестве данных URI непосредственно в исходном коде HTML. Это экономит нам накладные расходы на передачу заголовков HTTP, но кодирование base64 делает данные изображения на треть больше. Это релятивизируется, если вы поставляете HTML-код с содержанием, кодируя как gzip или brotli, но вы все равно должны использовать URIs данных для небольших изображений предварительного просмотра.
Гораздо более важным является тот факт, что предварительные изображения доступны немедленно и нет заметной задержки для пользователя при создании страницы.
Прежде всего, мы должны создать данные URI, которые мы затем используем в img
теге как src
. Для этого мы создаем данные URI через PHP, в котором этот код основан на только что созданном коде, который определяет смещения байт маркеров 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>
Вместо того, чтобы запрашивать данные через запрос Ajax, где мы сразу же получим каплю, в этом случае мы должны создать капля сами из данных URI. Для этого мы освобождаем данные-URI от части, которая не содержит данных изображений: data:image/jpeg;base64
. Мы расшифровываем оставшиеся базовые 64 закодированные данные с atob
помощью команды. Для того, чтобы создать каплю из теперь двоичных строк данных, мы должны передать данные в массив Uint8, который гарантирует, что данные не рассматриваются как ЗАкодированный текст UTF-8. Из этого массива теперь мы можем создать двоичную каплю с данными изображения изображения изображения предварительного просмотра.
Чтобы нам не пришлось адаптировать следующий код для этой водной версии, мы добавляем атрибут data-bytes
на img
тег, который в предыдущем примере содержит смещение байт, с которого должна быть загружена вторая часть изображения.
В сетевой вкладке консоли разработчика можно также проверить, что загрузка изображения предварительного просмотра не генерирует дополнительный запрос, в то время как размер файла страницы HTML увеличился.
Загрузка окончательного изображения
На втором этапе мы загружаем остальную часть файла изображения через две секунды в качестве примера:
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);
В заголовке диапазона на этот раз мы указываем, что мы хотим запросить изображение от конечного положения изображения предварительного просмотра до конца файла. Ответ на первый запрос хранится в src_part
свойстве объекта DOM. Мы используем ответы от обоих запросов для создания новой капли на new Blob()
, которая содержит данные всего изображения. URL-адрес blob, генерируемый из этого, снова используется по src
состоянию на объект DOM. Теперь изображение полностью загружено.
Также теперь мы можем проверить загруженные размеры в сетевой вкладке консоли разработчика снова.
Прототип
На следующем URL я предоставил прототип, где вы можете экспериментировать с различными параметрами: http://embedded-image-preview.cerdmann.com/prototype/
Репозиторий Прототипа GitHub можно найти здесь: https://github.com/McSodbrenner/embedded-image-preview
Рассмотрение в конце
Используя представленную здесь технологию Embedded Image Preview (EIP), мы можем загрузить качественно различные изображения предварительного просмотра с прогрессивных JEGs с помощью Ajax и HTTP Range Requests. Данные из этих изображений предварительного просмотра не отбрасываются, а вместо этого используются повторно для отображения всего изображения.
Кроме того, не нужно создавать изображения предварительного просмотра. На стороне сервера, только байт смещения, на котором предварительный просмотр изображения заканчивается должен быть определен и сохранен. В системе CMS должно быть возможно сохранить это число в качестве атрибута на изображении и принять его во внимание при вводе его в img
тег. Даже рабочий процесс был бы мыслим, который дополняет имя файла изображения смещением, например, прогрессивным-8343.jpg, для того, чтобы не сохранить смещение отдельно от файла изображения. Это смещение может быть извлечено кодом JavaScript.
Поскольку данные изображения предварительного просмотра используются повторно, этот метод может быть лучшей альтернативой обычному подходу загрузки изображения предварительного просмотра, а затем WebP (и предоставлению резервного копирования JPEG для браузеров, не поддерживающих WebP). Изображение предварительного просмотра часто уничтожает преимущества WebP, который не поддерживает прогрессивный режим.
В настоящее время предварительные изображения в обычном L-IP имеют низкое качество, так как предполагается, что загрузка данных предварительного просмотра требует дополнительной пропускной способности. Как Робин Осборн уже ясно в блоге в 2018году, это не имеет большого смысла, чтобы показать заполнителей, которые не дают вам представление об окончательном изображении. Используя технику, предложенную здесь, мы можем показать еще несколько окончательного изображения в качестве предварительного изображения без колебаний, представив пользователю более позднее сканирование прогрессивного JPEG.
В случае слабого сетевого подключения пользователя, может иметь смысл, в зависимости от приложения, не загружать весь JPEG, но, например, опустить последние два сканирования. Это производит гораздо меньше JPEG с лишь незначительно сниженным качеством. Пользователь будет благодарить нас за это, и мы не должны хранить дополнительный файл на сервере.
Теперь я желаю вам много веселья опробовать прототип и с нетерпением ждем ваших комментариев.
Источник: smashingmagazine.com