嵌入式图像预览加载图像速度更快

本文中介绍的嵌入式图像预览(EIP)技术允许我们使用渐进式JPEG,Ajax和HTTP范围请求在延迟加载期间加载预览图像,而无需传输其他数据。

低质量图像预览(LQIP)和基于SVG的变体SQIP是延迟图像加载的两种主要技术。两者的共同之处在于您首先生成低质量的预览图像。这将显示模糊,然后由原始图像替换。如果您可以向网站访问者呈现预览图像而无需加载其他数据,该怎么办?

根据规范,主要使用延迟加载的JPEG文件可以以这样的方式存储包含在其中的数据,即首先显示粗略的然后显示详细的图像内容。在加载(基线模式)期间,不是从上到下构建图像,而是可以非常快速地显示模糊图像,其逐渐变得更清晰和更锐利(渐进模式)。

在基线模式中表示JPEG的时间结构
基线模式(大预览)
以渐进模式表示JPEG的时间结构
渐进模式(大预览)

除了由更快速显示的外观提供的更好的用户体验之外,渐进式JPEG通常也小于其基线编码的对应物。对于大于10 kB的文件,根据雅虎开发团队的Stoyan Stefanov,使用渐进模式时,图像较小的概率为94%。

如果您的网站包含许多JPEG,您会注意到即使是渐进式JPEG也会依次加载。这是因为现代浏览器只允许六个同时连接到域。因此,单独的渐进式JPEG不是为用户提供最快的页面印象的解决方案。在最坏的情况下,浏览器将在开始加载下一个图像之前完全加载图像。

这里提出的想法现在只是从服务器加载渐进式JPEG的这么多字节,您可以快速获得图像内容的印象。稍后,在我们定义的时间(例如,当加载了当前视口中的所有预览图像时),应该加载图像的其余部分,而不再请求已经为预览请求的部分。

显示EIP(嵌入式图像预览)技术在两个请求中加载图像数据的方式。
使用两个请求加载渐进式JPEG(大预览)

遗憾的是,您无法告诉img属性中的标记应该在什么时间加载多少图像。但是,使用Ajax,只要提供映像的服务器支持HTTP范围请求,这是可能的。

使用HTTP范围请求,客户端可以在HTTP请求标头中通知服务器,所请求文件的哪些字节将包含在HTTP响应中。每个较大的服务器(Apache,IIS,nginx)都支持此功能,主要用于视频播放。如果用户跳到视频的末尾,则在用户最终看到所需部分之前加载完整视频效率不高。因此,服务器仅请求用户请求的时间周围的视频数据,以便用户可以尽可能快地观看视频。

1.创建渐进式JPEG

渐进式JPEG由几个所谓的扫描段组成,每个扫描段包含最终图像的一部分。第一次扫描仅非常粗略地显示图像,而文件后面的图像会向已加载的数据添加越来越多的详细信息,最终形成最终外观。

单个扫描的确切外观由生成JPEG的程序决定。在类似的命令行程序cjpeg依据从mozjpeg项目,甚至可以定义哪些数据这些扫描包含。但是,这需要更深入的知识,这超出了本文的范围。为此,我想参考我的文章“ Finally Understanding JPG ”,它讲授了JPEG压缩的基础知识。在wizard.txt中解释了必须在扫描脚本中传递给程序的确切参数mozjpeg项目。在我看来,默认情况下mozjpeg使用的扫描脚本(七次扫描)的参数是快速渐进结构和文件大小之间的良好折衷,因此可以采用。

要将我们的初始JPEG转换为渐进式JPEG,我们使用jpegtranmozjpeg项目。这是一个对现有JPEG进行无损更改的工具。Windows和Linux的预编译版本可在此处获得:https://mozjpeg.codelove.de/binaries.html。如果您出于安全考虑而喜欢安全地玩,那么最好自己构建它们。

从命令行我们现在创建渐进式JPEG:

$ jpegtran input.jpg > progressive.jpg

我们想要构建渐进式JPEG的事实由jpegtran假定,并且不需要明确指定。图像数据不会以任何方式更改。仅改变文件内的图像数据的排列。

理想情况下,应从JPEG中删除与图像外观无关的元数据(例如Exif,IPTC或XMP数据),因为如果元数据解码器位于图像内容之前,则相应的段只能被元数据解码器读取。由于这个原因我们无法将它们移动到文件中的图像数据后面,因此它们已经与预览图像一起传送并相应地放大第一个请求。使用命令行程序,exiftool您可以轻松删除这些元数据:

$ exiftool -all= progressive.jpg
复制

如果您不想使用命令行工具,还可以使用在线压缩服务compress-or-die.com生成不带元数据的渐进式JPEG。

2.确定第一个HTTP范围请求必须加载预览图像的字节偏移量

JPEG文件被分成不同的段,每个段包含不同的组件(图像数据,诸如IPTC,Exif和XMP之类的元数据,嵌入的颜色配置文件,量化表等)。这些段中的每一个都以由十六进制FF字节引入的标记开始。接下来是一个表示段类型的字节。例如,D8完成标记到SOI标记FF D8(图像开始),每个JPEG文件开始使用。

每次扫描开始都由SOS标记(扫描开始,十六进制FF DA)标记。由于SOS标记后面的数据是熵编码的(JPEG使用霍夫曼编码),因此FF C4在SOS段之前还有另一个具有解码所需的霍夫曼表(DHT,十六进制)的段。因此,渐进式JPEG文件中的我们感兴趣的区域由交替的霍夫曼表/扫描数据段组成。因此,如果我们想要显示图像的第一次非常粗略的扫描,我们必须FF C4从服务器请求直到第二次出现DHT段(十六进制)的所有字节。

在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的值添加到找到的位置,因为浏览器只在遇到新标记时才会呈现预览图像的最后一行(如前所述,它包含两个字节)。

由于我们对此示例中的第一个预览图像感兴趣,因此我们找到了正确的位置$positions[1],我们必须通过HTTP范围请求来请求该文件。要请求具有更好分辨率的图像,我们可以使用数组中的稍后位置,例如$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范围请求,它将以blob的形式返回HTTP-206响应(HTTP 206 =部分内容)中的二进制图像数据,我们可以使用它生成浏览器内部URL createObjectURL。我们将此网址用作src我们的img代码。因此我们加载了预览图像。

我们将blob另外存储在属性中的DOM对象上src_part,因为我们将立即需要这些数据。

在开发人员控制台的网络选项卡中,您可以检查我们是否未加载完整的图像,但只是一小部分。此外,应显示blob URL的加载,大小为0字节。

显示网络控制台和HTTP请求的大小
加载预览图像时的网络控制台(大预览

由于我们已经加载了原始文件的JPEG标题,因此预览图像的大小正确。因此,根据应用,我们可以省略img标签的高度和宽度。

替代方案:内嵌加载预览图像

出于性能原因,还可以将预览图像的数据直接作为数据URI传输到HTML源代码中。这节省了传输HTTP头的开销,但base64编码使图像数据增加了三分之一。如果您使用gzipbrotli等内容编码提供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>

我们不得不通过Ajax请求请求数据,而是立即收到blob,在这种情况下,我们必须自己从数据URI创建blob。为此,我们从不包含图像数据的部分释放数据URI : data:image/jpeg;base64. 我们用atob命令解码剩余的base64编码数据。为了从现在的二进制字符串数据创建blob,我们必须将数据传输到Uint8数组,这确保数据不被视为UTF-8编码的文本。从这个数组中,我们现在可以使用预览图像的图像数据创建二进制blob。

因此,我们不必为此内联版本调整以下代码,我们data-bytesimg标记上添加属性,在前面的示例中包含必须加载图像的第二部分的字节偏移量。

在开发人员控制台的网络选项卡中,您还可以在此处检查加载预览图像不会生成其他请求,而HTML页面的文件大小已增加。

显示网络控制台和HTTP请求的大小
将预览图像加载为数据URI时的网络控制台(大预览

加载最终图像

在第二步中,我们在两秒后加载图像文件的其余部分作为示例:

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标题中,这次我们指定要从预览图像的结束位置到文件末尾请求图像。第一个请求的答案存储在src_partDOM对象的属性中。我们使用两个请求的响应来创建一个新的blob per new Blob(),其中包含整个图像的数据。从此生成的blob URL再次用作srcDOM对象。现在图像已完全加载。

此外,我们现在可以再次检查开发者控制台的网络选项卡中的加载大小。

显示网络控制台和HTTP请求的大小
加载整个图像时的网络控制台(31.7 kB)(大预览

原型

在以下URL中,我提供了一个原型,您可以在其中试验不同的参数:http//embedded-image-preview.cerdmann.com/prototype/

可以在此处找到原型的GitHub存储库:https//github.com/McSodbrenner/embedded-image-preview

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/261681.html

(0)
上一篇 2022年5月24日
下一篇 2022年5月24日

相关推荐

发表回复

登录后才能评论