如何通过嵌入式图片预览快速加载图片

摘要:本文介绍的嵌入式图像预览(Embedded Image Preview, EIP)技术允许我们在延迟加载期间可以使用渐进式jpegAjaxHTTP范围请求加载预览图像,而不需要传输额外的数据。


低质量图片预览(LQIP)和基于SVG的SQIP是懒加载的两种主要形式。这两者的共同之处都是一开始你加载的是一张低质量,显示的比较模糊的预览图片,然后用一张原始的清晰的图片去替换的一个过程。如果你可以在没有额外数据加载的同时,实现向网站访问者显示预览图片,那会怎么样呢?


加载JPEG格式的文件,可能是我们用懒加载用的最多的地方。根据规范,利用首先展示比较模糊或者比较低质量的图片,然后显示更清晰或者质量更高的图片的方式存储其中包含的数据。在加载过程中,并不是从顶部到底部以基线为标准的显示图片的方式,而是快速的展示清晰的图片,图片是以渐进的形式变得越来越清晰。

如何通过嵌入式图片预览快速加载图片

基线模式加载图片 

如何通过嵌入式图片预览快速加载图片

渐进式加载图片 


除了通过更加快速的显示外观的方式来优化用户的体验之外,渐进式的JPEG通常也比以基线为标准加载的JPEG更加小点。根据Yahoo开发团队的Stoyan Stefanov 的说法,当文件的大小超过10kb的时候, 使用渐进式的模式加载图片的话,大概有94%的图片会被压缩


如果你的网站由很多的JPEG组成,想必你也会注意到即使是渐进式的JPEG也是会一个接着一个的加载的。这就是为什么现代浏览器只允许同时加载6个请求。


因此,渐进式的JPEG并不是快速向用户展示完整页面的解决方案。最坏的情况是,浏览器在完整的加载完成一张图片之后才会继续加载另一张。


这里目前提出的想法是从服务器端加载一定字节的JPEG,这样的话你能够快速获得图片内容的初步映像。然后,在我们指定的时间内(这个我们指定的时间指的是例如当前时间所有的预览图像已经被加载),剩下的图片被加载,而不需要再一次请求已经被预览的部分的数据。

如何通过嵌入式图片预览快速加载图片

 加载具有两个请求的渐进式JPEG(大预览)


然而不幸的是,你并不能够用一个属性的方式告诉图片标签它应该用多长时间去加载。然而,对于Ajax,这个是有可能实现的,但是前提是提供图片的服务器需要支持 http range request (即http范围请求)。


使用http范围请求,客户端可以通过http请求的请求头告诉服务器哪些关于请求文件的信息可以被包含在http的响应里面。较大的服务器(例如:Apache,IIS,NGINX等等)基本都支持这个特性,主要用于视频回放。如果用户跳转到视频的末尾,在用户最终看到所需的部分之前加载完整的视频不是很有效。因此,只有用户请求的时间前后的视频数据才会被服务器请求,这样用户才能尽可能快的观看视频。


我们目前需要面对的是以下3个挑战:

1、创建这个渐进式的JPEG

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

3、创建前端javascript代码 



1、创建这个渐进式的JPEG


一个渐进的jpeg由所谓的几个扫描片段组成,每一个片段包含一部分的最终图片的信息。第一次扫描仅仅是很粗糙的显示这张图片,以后每一次的扫描向已经加载的文件中添加越来越详细的信息,直至最终完整展示。


单个生成的外观由生成jpeg的程序确定。在诸如来自mozjpeg项目的cjpeg这样的命令行程序中,你甚至可以定义这些扫描包含哪些数据。但是,这需要更深入的知识,这超出了本文的范围。为此,我想参考我的这篇文章”Finally Understanding JPG”(文章链接:https://compress-or-die.com/Understanding-JPG),这篇文章讲述了JPEG压缩的基础知识。在mozjpeg项目的wizard.txt中解释了扫描脚本中必须传递给程序的确切参数。


在我看来,默认情况下,mozjpeg使用的扫描脚本(7次扫描)的参数是快速渐进结构和文件大小之间的一个很好的折中,因此可以采用。


要将初始的jpeg转换成渐进的jpeg,我们需要借助于mozjpeg中的jpegtran。这是一个将jpeg进行无损改变的一个工具。这里提供了针对于Windows和linix系统的预编译版本 : 

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,潜入的颜色配置文件,量化表等等)。每个片段都以一个十六进制的FF字节引入的标记开始,然后是一个字节(指示段的类型)。例如,D8完成对SOI标记FF D8(图像的开始)的标记,每个JPEG文件都以FF D8开始。


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

如何通过嵌入式图片预览快速加载图片

JPEG文件的结构(大预览)

 

在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范围请求进行请求数据,要一个分辨率更高的图片,我们需要一个相对靠后的位置,例如$positions[3]


3、创建前端javascript代码


首先,我们需要定一个图片标签,我们给这个图片赋值的字节位置在: 

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


与延迟加载库的通常情况一致,我们不直接定义src属性,因此,浏览器在解析HTML代码时不会直接开始从服务器请求图片。

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的范围请求,它会返回以HTTP-206响应(HTTP 206 =部分内容)以blob的形式返回二进制图像数据,从中我们可以用createObjectURL生成浏览器内部的URL。我们可以将这个URL作为img标签的src属性的值,这样我们就可以加载我们的预览图片。


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


在浏览器的开发者模式中的network里面,我们可以看到并没有完整加载了这个图片,而只是加载了一小部分。此外blob url的加载应该以0字节的形式显示。 


如何通过嵌入式图片预览快速加载图片

加载预览图像时的网络控制台(大预览)


因为我们已经加载了原始文件的jpeg头,所以预览图片的大小是正确的。此外,根据应用程序的不同,我们可以省略img标签的宽和高。



备选方案:内联加载预览图像


出于性能原因的考虑,也可以在HTML源代码中直接将预览图像的数据作为数据URI传输。这节省了传输HTTP头信息的时间,但是base64编码使图像数据变大了三分之一。


如果您使用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);

现在我们直接将img标签中的src的值替换成URI数据

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


 当然,下面这段js代码也是必须有的

<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>


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


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


在浏览器的开发者模式中的network里面,你可以看到预览图片被加载了,但是并没有产生额外的请求,并且HTMl页面的大小已经增加了。 

如何通过嵌入式图片预览快速加载图片

在将预览图像作为数据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);

 

在这次的范围头中,我们指定的请求信息的位置是从预览图片的结束位置到文件的结束位置。第一次请求的返回值,我们存在dom元素的src_part 属性中。我们用这两次的请求的响应为每个新的blob创建一个新的blob。这包含了整个图片的信息。由此生成的blob URL再次用作DOM对象的src。现在图像已完全加载。并且,我们在浏览器的开发者模式下,可以再次查看加载的大小。 

如何通过嵌入式图片预览快速加载图片加载整个图像(31.7 kB)时的网络控制台(大预览)

原型

在下面的URL中,我提供了一个可以使用不同参数进行试验的原型:

http://embedimage-preview.cerdmann.com/prototype/


原型的GitHub存储库可以在这里找到:

https://github.com/mcsodbrenner/embedimage -preview


最后的考虑事项


使用本文介绍的嵌入式图像预览(EIP)技术,我们可以在Ajax和HTTP范围请求的帮助下从渐进jpeg加载不同质量的预览图像。这些预览图像中的数据不会被丢弃,而是重新用来显示整个图像。


此外,不需要创建预览图像。在服务器端,只需要确定并保存预览图像结束时的字节偏移量。在CMS系统中,应该可以将这个数字保存为图像的一个属性,并在img标记中输出它时考虑到它。甚至可以想象一个工作流,它用偏移量来补充图片的文件名,例如progressive-8343.jpg,以便不必将偏移量从图片文件中保存出来。这个偏移量可以由JavaScript代码提取。


由于预览图像数据是重用的,因此这种技术可以更好地替代通常的方法,即先加载预览图像,然后加载WebP(并且为不支持web的浏览器提供JPEG回退)。预览图像常常会破坏WebP的存储优势,因为它不支持渐进模式。


目前,普通LQIP中的预览图像质量较差,因为加载预览数据需要额外的带宽。正如罗宾·奥斯本(Robin Osborne)在2018年的一篇博客文章中明确指出的那样,如果占位符没有给出最终图像的概念,那么它就没有多大意义。通过使用这里建议的技术,我们可以毫不犹豫地将更多的最终图像显示为预览图像,方法是向用户提供渐进式JPEG的后续扫描。


如果用户的网络连接很弱,根据应用程序的不同,不加载整个JPEG是有意义的,但是,例如,省略最后两次扫描。这将生成一个小得多的JPEG,其质量仅略有下降。用户会为此感谢我们,我们不必在服务器上存储额外的文件。


现在,我希望您在试用这个原型时能获得很多乐趣,并期待您的评论。



本文转载自:

https://www.smashingmagazine.com/2019/08/faster-image-loading-embedded-previews/

作者:Christoph Erdmann

翻译:吴涛

本篇文章来源于微信公众号: TripDesign

UI/UX

不需要画图?只靠写字就能slay全场的设计师有多爽!

2019-10-12 17:00:00

UI/UX

「惊喜盒子」小游戏设计背后的故事

2019-10-14 10:59:26

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索