webp 图片适配流量优化
asfe9556
8年前
<h2>图片流量优化</h2> <p>刷新一个页面消耗的流量除了脚本样式文件以外,大头其实在下载的图片。一张图片动辄几十kb,想尽办法优化样式、脚本文件所优化的图片流量其实还不如一张图片大。</p> <p>本文从两个角度介绍如何对图片流量进行优化。本文进行图片流量优化的前提都是对于 <strong>移动端</strong> 而言。</p> <h2>webp</h2> <p>首先从图片格式方面着手,webp( <a href="/misc/goto?guid=4959630046995743016" rel="nofollow,noindex">google官方网址</a> )是谷歌推出的一种图片格式,优点在于同等画面质量下,体积比jpg、png少了25%以上。以两张jpg、png图片为例:</p> <ol> <li> <p>JPG <a href="https://simg.open-open.com/show/50bb481d5a3aebcf802b4d5062968fcf.jpg" rel="nofollow,noindex">http://cdn1.showjoy.com/images/c9/c9c2221942774550ad53342da23774de.jpg</a></p> </li> <li> <p>PNG <a href="https://simg.open-open.com/show/17aa2f8f0ed02580e09ff5360369e883.png" rel="nofollow,noindex">http://cdn1.showjoy.com/images/bb/bb1c8b0d275e4ba2903dc822a03add50.png</a></p> </li> </ol> <table> <thead> <tr> <th>size</th> <th>JPG</th> <th>PNG</th> </tr> </thead> <tbody> <tr> <td>无压缩</td> <td>165kb</td> <td>55kb</td> </tr> <tr> <td>tinypng压缩</td> <td>75kb</td> <td>20kb</td> </tr> <tr> <td>webp转换</td> <td>54kb</td> <td>6.1kb</td> </tr> </tbody> </table> <p>由表格的罗列可知,将图片转换为webp格式,图片的体积比tinypng压缩完后的体积还要小,且图片质量甚至还要高于tinypng压缩。</p> <p>虽然webp格式的图片相对于png和jpg体积小质量高,但是目前的兼容性在全球范围只达到了70%左右。<img src="https://simg.open-open.com/show/1ed3acf2969ff660c6abff46335e7fe0.png"></p> <p>根据caniuse,目前在移动端安卓机型4.4以上全部支持,但是ios全军覆灭。我司用户ios、安卓55分成,支持了webp至少能为一半用户提供更小体积的图片体验。而且据说ios10系统将支持webp,这样一来我司产品的webp支持度将会更高。 </p> <h3>图片服务器支持webp转换</h3> <p>我司原本就有基于 nginx+lua+graphicsmagick 的图片缩略图功能,具体使用方法类似</p> <p>http://cdn1.shwojoy.com/images/34/xxxxx.png</p> <p>http://cdn1.shwojoy.com/images/34/xxxxx.png.300x300.png</p> <p>为了支持webp转换需要修改原先的lua脚本,添加对 .webp 后缀的识别。使之能对类似 xxxxx.png.300x300.png.webp 或者 xxxxx.png.webp 这样的域名进行识别并转换。</p> <p>nginx+lua+graphicsmagick 这套方案其实做的事情就是 nginx 对域名进行拦截, lua 脚本进行域名后缀规则的匹配,比如说 300x300.png / .webp 类似的后缀,匹配完成后再在lua里调用 graphicsmagick 的命令,进行一些图片转换、裁剪等工作。</p> <p>lua脚本片段</p> <pre> <code class="language-javascript">if table.isLegal(size_list) and extend == "webp" then command = [[/usr/local/GraphicsMagick-1.3.25/bin/gm convert -quality 75 -density 72 +profile "*" ]] .. ngx.var.image_root .. originalUri .. " -geometry " .. area .. " " .. ngx.var.file; os.execute(command); end</code></pre> <p>值得注意的是graphicsmagick版本在1.3.20及以上才支持webp download</p> <p>graphicsmagick能做到转换webp还需要下载编译libwebp。 </p> <p>图片服务器支持webp转换后,就能实时转化webp格式的图片了,为接下来的webp兼容方案提供了技术支持。</p> <ul> <li><a href="https://simg.open-open.com/show/50bb481d5a3aebcf802b4d5062968fcf.jpg" rel="nofollow,noindex">http://cdn1.showjoy.com/images/c9/c9c2221942774550ad53342da23774de.jpg</a></li> <li><a href="/misc/goto?guid=4959739497052519737" rel="nofollow,noindex">http://cdn1.showjoy.com/images/c9/c9c2221942774550ad53342da23774de.jpg.webp</a></li> </ul> <h3>webp兼容方案</h3> <p>目前在浏览器端判断是否支持webp最好的方法就是特性检测法。根据检测结果将是否支持webp的值存入cookie,供之后需要判断webp兼容性的地方使用。</p> <p>特性检测脚本:</p> <pre> <code class="language-javascript">;(function(doc) { // 给html根节点加上webps类名 function addRootTag() { doc.documentElement.className += " webpa"; } // 判断是否有webp_showjoy=available这个cookie if (!/webp_showjoy=available/.test(document.cookie)) { var image = new Image(); // 图片加载完成时候的操作 image.onload = function() { // 图片加载成功且宽度为1,那么就代表支持webp了,因为这张base64图是webp格式。如果不支持会触发image.error方法 if (image.width == 1) { // html根节点添加class,并且埋入cookie addRootTag(); document.cookie = "webp_showjoy=available; max-age=31536000; domain="; } }; // 一张支持alpha透明度的webp的图片,使用base64编码 image.src = 'data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAARBxAR/Q9ERP8DAABWUDggGAAAABQBAJ0BKgEAAQAAAP4AAA3AAP7mtQAAAA=='; } else { addRootTag(); } }(document));</code></pre> <p>如果浏览器支持webp格式的图片则在cookie中设置标志,并且在html标签上设置 webpa className,这个classname的作用是在less文件中兼容webp图片。</p> <p>less文件中的webp兼容</p> <pre> <code class="language-javascript">.webpbg(@url) { background-image: url(@url); .webpa & { background-image: url('@{url}.webp'); } }</code></pre> <p>这段less取代了原先在less文件中描述背景图片的代码。</p> <p>比如原先定义背景图片</p> <pre> <code class="language-javascript">div { background-image: url(xx); }</code></pre> <p>现在使用兼容方案则为:</p> <pre> <code class="language-javascript">div { webpbg(xxx); }</code></pre> <p>html文件中的webp图片兼容</p> <p>我司目前html部分通过java的velocity编写,对于同步传递到页面上的图片webp将通过如下方式兼容。</p> <pre> <code class="language-javascript"><img class="slider-img" src="$!{banner.recordMap.get('图片地址').value}.750x448.jpg$!{isWebp}"></code></pre> <p>$!{isWebp} 变量是后台通过判断浏览器请求的cookie中是否有之前定义的 webp_showjoy=available ,有则返回 .webp 后缀,无则返回空。</p> <p>这样有个问题是用户第一次浏览产品页面时后台判断cookie永远是false,所以用户第一次浏览是不可能返回 .webp 后缀的图片的。</p> <p>还有一种情况是图片大量使用的时候我们会使用懒加载进行图片的延迟加载。这时就可以修改懒加载插件,在插件里动态兼容webp图片了。</p> <pre> <code class="language-javascript">/* 根据cookie返回图片是否webp的地址 */ function getwebpsrc (imgsrc) { var needwebp = false, src = ''; if (/webp_showjoy=available/.test(document.cookie)) { needwebp = true; } src = needwebp ? imgsrc + '.webp' : imgsrc; return src; }</code></pre> <p>到此就完成了移动端对webp格式图片的支持。这也是图片流量优化其中之一方案。</p> <h2>retina兼容图片流量优化</h2> <p>前端应该都了解在retina屏下应该使用 @2x 或者 @3x 等倍率的图片,才能保证图片的清晰度。但是为了切图方便,部分公司都会统一在切图阶段切出 @2x 的图片,不管浏览设备是retina屏还是普通屏,一律都使用 @2x 的图片。这样做有两个坏处,一是 @2x 的图片在非retina屏下会出现downsampled现象,虽然不会影响清晰度,但是会缺少一些锐利度。二是 @2x 的图片相比较 @1x 的图片,前者体积大于后者,这也就造成了流量的浪费以及影响页面打开性能。</p> <p>所以正确处理方法是针对retina屏的是否采用不同尺寸的图片。图片裁剪已经在图片服务器上实现。在考虑retina时也需要加上webp的兼容,两者一起作用会大大减少图片的尺寸。</p> <pre> <code class="language-javascript">@2x http://cdn1.showjoy.com/images/bb/bb1c8b0d275e4ba2903dc822a03add50.png.300x300.png.webp @1x http://cdn1.showjoy.com/images/bb/bb1c8b0d275e4ba2903dc822a03add50.png.150x150.png.webp</code></pre> <h3>less文件兼容retina</h3> <pre> <code class="language-javascript">.retinabg(@file-2x; @reg-2x; @reg-1x; @type) when (isstring(@reg-2x)) { background-image: url("@{file-2x}.@{reg-1x}.@{type}"); .webpa & { background-image: url('@{file-2x}.@{reg-1x}.@{type}.webp'); } @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and ( min--moz-device-pixel-ratio: 2), only screen and ( -o-min-device-pixel-ratio: 2/1), only screen and ( min-device-pixel-ratio: 2), only screen and ( min-resolution: 192dpi), only screen and ( min-resolution: 2dppx) { background-image: url("@{file-2x}.@{reg-2x}.@{type}"); .webpa & { background-image: url('@{file-2x}.@{reg-2x}.@{type}.webp'); } } }</code></pre> <p>当要代替原先书写时的 background-image: url() ,可以写成如下方式:</p> <pre> <code class="language-javascript">.retinabg(http://cdn1.showjoy.com/images/bb/bb1c8b0d275e4ba2903dc822a03add50.png; 300x300; 150x150; png)</code></pre> <h3>html文件兼容retina</h3> <p>在html中本次方案准备使用html5特性 srcset 属性。</p> <p>secset 属性的目的在于允许开发者为某个图片的属性指定一系列的来源,其中这些图片的来源是要根据客户端显示屏的像素分辨率来设定的。</p> <p>比如在volecity模板中定义图片资源:</p> <pre> <code class="language-javascript"><img class="pic" src="$!{newProduct.image}.300x300.png$!{isWebp}" srcset="$!{newProduct.image}.150x150.png$!{isWebp} 1x, $!{newProduct.image}.300x300.png$!{isWebp} 2x"></code></pre> <p>这段图片定义说明了在retina(2x)屏幕下使用300x300的图片来源,而在飞retina屏下(1x)下使用150x150的图。</p> <p>html中懒加载的图片</p> <p>由于懒加载的图片在插件中就会进行src赋值的操作,所以直接在懒加载插件中根据 window.devicePixelRatio 进行判断修改图片url。</p> <pre> <code class="language-javascript">/* 根据cookie返回图片是否webp的地址 */ /* 根据dpr返回不同尺寸的图片 */ function getwebpsrc (imgsrc) { var areaInfo = ''; if (window.devicePixelRatio && window.devicePixelRatio <= 1) { var area = imgsrc.match(/[0-9]+x[0-9]+/); if (area) { var areaSplit = area[0].split('x'); areaInfo = areaSplit[0] /2 + 'x' + areaSplit[1] /2; imgsrc = imgsrc.replace(/[0-9]+x[0-9]+/, areaInfo) } } var needwebp = false, src = ''; if (/webp_showjoy=available/.test(document.cookie)) { needwebp = true; } src = needwebp ? imgsrc + '.webp' : imgsrc; return src; }</code></pre> <p>在这个方案下所有图片的编写都必须要带上尺寸后缀(_num_x_num_),在我司图片服务器支持下还有一个特别的好处:图片服务器会对带有尺寸后缀的图片进行尺寸裁剪的同时进行图片压缩,遇到不支持webp格式的浏览器上就会顺带对图片进行压缩,尽力地减小图片体积。</p> <p><img src="https://simg.open-open.com/show/db64b3faa8878d9ffa37da37091934aa.png"></p> <p>srcset 属性目前在移动端的兼容性十分不错,安卓4.x版本不支持。</p> <h2>总结</h2> <p>一共两点图片流量优化方案,一是针对webp图片格式,二是针对retina。最后总结下来的编写图片代码的情景就为3种:</p> <p>1.html同步图片编写</p> <pre> <code class="language-javascript"><img class="pic" src="$!{newProduct.image}.300x300.png$!{isWebp}" srcset="$!{newProduct.image}.150x150.png$!{isWebp} 1x, $!{newProduct.image}.300x300.png$!{isWebp} 2x"></code></pre> <p>2.懒加载图片编写</p> <pre> <code class="language-javascript"><img class="goods-pic j_Lazyload" data-original="{{$value.image}}.300x300.png" src="https://simg.open-open.com/show/a3b244c260fdb7164e263238fad130a7.png"></code></pre> <p>3.less</p> <pre> <code class="language-javascript">.retinabg(http://cdn1.showjoy.com/images/bb/bb1c8b0d275e4ba2903dc822a03add50.png; 300x300; 150x150; png) webpbg(http://cdn1.showjoy.com/images/bb/bb1c8b0d275e4ba2903dc822a03add50.png);</code></pre> <p>对我以上的理解有疑问和意见的欢迎找我私聊~微博-写前端的暹罗</p> <p> </p> <p>来自:https://github.com/ShowJoy-com/showjoy-blog/issues/10</p> <p> </p>