移动端 HTML5 项目使用 WebP 图片探讨

引言

WebP是Google开发的一种图片文件格式,同时支持图片的无损压缩和有损压缩。WebP最初的目标是为了减少文件大小,从而加快网络上的页面访问速度。在相同的图片质量下,无损压缩图片比PNG格式小26%,有损压缩图片比JPEG小25~34%1

根据官方的数据,WebP格式有不小的优势——大概有26%的带宽节约。事实上WebP在2010年发布,至今已经接近7年。在2013年我从事手游研发时项目已经全线引入了WebP。如果项目对图片质量要求比较高、图片资源又比较大的话,还是值得引入的。但是HTML5项目2由于本身的一些特殊性,使用WebP图片还是有不少的问题。

另外一点,出于政治(简单来说就是 Google 喜欢推行自己的技术标准,但是 Mozilla 等不一定全盘接受)和WebP图片本身缺陷的原因,未来 FireFox/Edge/Safari 等浏览器对于 WebP 的支持也没有完全确定。

图片格式对比

根据Google官方介绍的数据,WebP平均比PNG图片小26%。在讨论方案前,我挑选了以前项目中用到的60张PNG图片做了对比测试(由于大部分项目使用的图片都是带透明通道的PNG图片,JPEG相对较少,所以只对比了PNG图片)。

PNG图片压缩

PNG图片的压缩分为无损压缩和有损压缩两种,无损压缩一般是用ImageOptim去除图片中的冗余信息(大概可以压缩5%左右);有损压缩一般是从PNG24格式压缩为PNG8格式,pngquant是一个比较流行的库,可以使用image-webpack-loader每次打包时自动处理,也可以使用ImageAlpha一次批量处理好放入项目中。

实践中压缩PNG图片的比较好的方法是pngquant + ImageOptim,流程大致如下:

Every Bit Counts.

为了公平对比,使用WebP无损压缩对比PNG无损压缩,WebP有损压缩对比PNG8有损压缩。对于用户访问网络页面来说,Every Bit Counts,很多时候我们会牺牲一些色彩度来大幅度压缩文件(实际上,HTML5项目中很多图片都是处理成PNG8来大幅度提高页面的访问速度),所以PNG8的参考也是必要的——毕竟引入新技术要比生产环境有优势。

WebP vs PNG24 vs PNG8

为了验证官方数据,我找了一些资源做测试,数据生成方式:

  1. 原始图片文件:UI提供,大部分是PhotoShop导出,所以其中不包含照片;
  2. WebP Lossless:通过cwebp -q 100 "$file" -o "$file.webp" -lossless生成
  3. PNG24:ImageOptim批量压缩生成
  4. WebP Lossless vs PNG24:(PNG24大小 - WebPLossless大小) / 原始大小
  5. WebP Loss:通过cwebp -q 100 "$file" -o "$file.webp"生成3
  6. PNG8:pngquant --force --ext .png *.png -> ImageOptim批量压缩生成
  7. WebP vs PNG8:(PNG8大小 - WebPLoss大小) / 原始大小

所得数据如下:

图片 原始大小(字节) WebP Lossless PNG24 WebP Lossless vs PNG24 WebP Loss PNG8 WebP vs PNG8
image1.png 634,022 456,208 567,430 -17.54% 192,208 258,189 -10.41%
image2.png 396,419 108,574 212,323 -26.17% 99,616 86,209 3.38%
image3.png 202,683 100,652 141,316 -20.06% 81,742 55,635 12.88%
image4.png 184,871 66,424 100,781 -18.58% 49,570 41,843 4.18%
image5.png 173,382 85,234 98,493 -7.65% 43,020 39,856 1.82%
image6.png 173,382 85,234 98,493 -7.65% 43,020 39,856 1.82%
image7.png 92,715 37,084 58,920 -23.55% 62,282 31,929 32.74%
image8.png 88,804 58,560 74,963 -18.47% 72,940 30,366 47.94%
image10.png 81,941 41,132 46,453 -6.49% 21,864 20,795 1.30%
image9.png 81,941 41,132 46,453 -6.49% 21,864 20,795 1.30%
image11.png 50,071 18,500 29,617 -22.20% 15,948 16,072 -0.25%
image12.png 38,768 14,556 30,474 -41.06% 32,098 15,727 42.23%
image13.png 32,019 20,544 22,851 -7.21% 10,120 17,474 -22.97%
image14.png 25,142 1,876 2,893 -4.05% 3,370 2,876 1.96%
image15.png 24,934 1,688 2,647 -3.85% 2,956 2,638 1.28%
image16.png 24,775 1,708 2,686 -3.95% 2,986 2,672 1.27%
image17.png 24,268 1,916 2,873 -3.94% 3,224 2,862 1.49%
image18.png 23,764 14,062 15,699 -6.89% 7,796 12,053 -17.91%
image19.png 23,324 11,414 16,165 -20.37% 19,610 8,739 46.61%
image20.png 23,274 1,332 1,971 -2.75% 1,794 2,169 -1.61%
image21.png 23,083 9,866 17,368 -32.50% 8,366 9,856 -6.45%
image22.png 22,347 1,342 1,998 -2.94% 1,866 2,177 -1.39%
image23.png 21,356 8,946 10,899 -9.14% 16,456 9,089 34.50%
image24.png 21,249 8,648 11,021 -11.17% 16,108 6,719 44.19%
image25.png 21,092 11,978 14,861 -13.67% 6,412 11,951 -26.26%
image26.png 18,371 7,670 9,797 -11.58% 13,012 8,376 25.24%
image27.png 17,826 7,174 8,962 -10.03% 12,464 5,934 36.63%
image28.png 17,300 8,012 12,771 -27.51% 9,196 8,960 1.36%
image29.png 17,036 6,964 8,958 -11.70% 11,788 5,105 39.23%
image30.png 15,814 8,192 10,126 -12.23% 5,156 8,222 -19.39%
image31.png 14,561 6,836 10,380 -24.34% 5,432 6,708 -8.76%
image32.png 13,288 5,512 7,129 -12.17% 10,156 4,085 45.69%
image33.png 13,225 6,960 7,645 -5.18% 17,464 6,683 81.52%
image34.png 11,500 4,990 8,277 -28.58% 9,354 4,878 38.92%
image35.png 11,307 610 1,355 -6.59% 2,168 1,355 7.19%
image36.png 11,239 588 1,355 -6.82% 2,582 1,355 10.92%
image37.png 8,703 546 1,162 -7.08% 1,836 1,162 7.74%
image38.png 8,468 2,928 4,335 -16.62% 4,892 4,036 10.11%
image39.png 6,665 2,944 4,350 -21.10% 3,258 3,818 -8.40%
image40.png 5,382 360 794 -8.06% 1,378 794 10.85%
image41.png 5,362 372 794 -7.87% 1,160 794 6.83%
image42.png 5,234 1,384 1,689 -5.83% 4,476 1,689 53.25%
image43.png 4,854 1,684 2,457 -15.93% 3,806 2,457 27.79%
image44.png 4,725 774 1,369 -12.59% 1,946 1,369 12.21%
image45.png 4,314 336 703 -8.51% 1,068 703 8.46%
image46.png 3,831 1,122 1,398 -7.20% 4,714 1,398 86.56%
image47.png 3,114 1,198 1,715 -16.60% 2,444 1,715 23.41%
image48.png 3,107 1,018 1,268 -8.05% 2,862 1,268 51.30%
image49.png 2,832 880 1,408 -18.64% 2,450 1,408 36.79%
image50.png 2,825 862 1,070 -7.36% 2,632 1,070 55.29%
image51.png 2,642 874 1,379 -19.11% 2,404 1,317 41.14%
image52.png 2,611 970 1,421 -17.27% 2,176 1,473 26.92%
image53.png 2,581 486 794 -11.93% 1,150 794 13.79%
image54.png 2,464 834 1,288 -18.43% 2,472 1,256 49.35%
image55.png 2,316 924 1,290 -15.80% 2,006 1,343 28.63%
image56.png 2,310 756 950 -8.40% 3,042 950 90.56%
image57.png 2,254 628 1,065 -19.39% 1,306 1,041 11.76%
image58.png 2,035 732 961 -11.25% 1,846 961 43.49%
image59.png 1,988 738 1,079 -17.15% 2,152 1,112 52.31%
image60.png 1,818 748 1,031 -15.57% 1,998 1,067 51.21%
总计 2,763,528 1,296,216 1,752,173 -16.50% 995,482 845,203 5.44%

从表格中可以看出,无损压缩对比中,总体WebP格式比PNG24格式平均节约了16.50%的大小,在所有尺寸下WebP表现都比PNG24优秀。

对比有损压缩来看,PNG8略胜WebP,不过总体也只小5.44%。另一方面,小文件的压缩(13KB以下)PNG8有很大的优势。大部分情况下PNG8压缩的情况都比WebP好。

这样看来,使用WebP图片,无损压缩格式还是有一定优势;如果项目主要用损压缩PNG8,切换WebP可能没有太大的优势

浏览器兼容WebP图片现状

CanIUse.com的WebP兼容数据我们可以看到,只有Chrome/Android浏览器(Google自家的)和Opera浏览器有对WebP图片的支持,IE/Edge/Firefox/Safari系列都不支持。

即使是全球范围内也只有72.9%的兼容率,中国地区差不多是72.58%。所以如果要使用WebP图片需要考虑一大部分浏览器不兼容的现状。

如何解决兼容性问题

作为一名端工程师,很大一部分的工作内容都与兼容性相关。总结来说,根据资源从获取到展示的流程,解决兼容性有两个方向的思路:

  1. 拦截器(Interceptor)

    拦截器的作用是在获取资源的出入口进行拦截,拦截后可以做替换、转换等操作。拦截可以通过代码自身完成,也可以借助外部环境完成。

  2. 解析器(Parser)

    解析器即在运行环境注入解析库,使得运行环境能够读取和解析对应格式的资源。

    对于客户端(iOS, Android等)来说,各种第三方库都可以说是解析器,如果平台不支持WebP格式解析,可以打包第三方库(比如libwebp)到App包里。客户端解决兼容性相对容易一点,因为基本上使用图片只有一个入口引用,分别在ObjectiveC/Swift/Java代码中。而HTML5项目中则对应着HTML/CSS/JS三个入口,我们注入第三方库(比如libwebpjs)只能解决JS引用的问题。

    所以对于浏览器来说,兼容性处理就麻烦的多。浏览器是各个厂商提供的,我们也不能强行让用户给浏览器装插件。另一方面,HTML5的发展趋势也是去插件化,从Flash,WebGL等技术的发展趋势也能够看出。插件这一条路也是不可取的。

HTML5 WebP 解决方案

根据前面的讨论,项目中引入WebP有一定的优势,那么要在HTML5项目使用WebP图片,解决方案有哪些呢?

  1. <picture>标签

    <picture>标签是根据浏览器能力自动选择图片格式的解决方案,思路很不错,目前其兼容性如下:

    可以看到大部分浏览器已经支持了这个标签。主要问题在于Android平台在Android 5-6.x才支持。根据Android官方统计数据,目前还有接近50%的Android设备是在5.0以下的。

    iOS平台在9.3后的系统才支持这个标签。根据David Smith的统计数据,iOS还有大概18%的设备是10.x以下的。

    TODO 在不支持<picture>标签的浏览器上运行能否正常显示图片。

    • 优点是通用性好,使用容易,不支持WebP格式的可以自动fallback到其他格式。
    • 缺点是iOS和Android均有一部分比例的系统不支持此标签。
  2. 客户端拦截:如果项目只运行在客户端(比如iOS,Android的Webview)环境中,可以在客户端进行拦截Webview中的图片引用,将WebP图片转码成PNG/JPEG后再返回给Webview。

    • 优点是实现相对比较容易,只需要iOS端作处理即可,Android和HTML5端不需要处理。
    • 缺点是与运行环境耦合,脱离了特定的环境无法运行,对于需要同时支持客户端和浏览器环境的项目不适用;另一方面,转码很消耗客户端性能。
  3. 资源加载拦截:JS实现统一的资源加载器,所有的图片资源加载时,加载器根据浏览器的能力去分别加载PNG/WebP格式资源。

    • 优点是通用性好,维护容易,只需要在JS入口做控制。
    • 缺点是无法处理HTML/CSS中的图片引用,只对游戏等特定应用场景适用。
  4. 多版本代码:根据浏览器是否支持WebP,加载不同版本的JS和CSS文件。大部分工作在编写构建流程,业务逻辑本身不用太多改动。

    • 优点是通用性好,能适应不同的运行环境,HTML,JS和CSS都能控制。
    • 缺点是打包流程会变得比较复杂,需要预先生成多套代码和资源。另一方面生成的一套WebP代码和资源只在Android平台有用。
  5. libwebpjs:libwebpjs能够通过js,在WebP和其他图片格式之间进行encode和decode,使得不支持WebP格式的浏览器也能够解析图片。

    • 优点是能在所有浏览器享受WebP格式的优势。
    • 缺点是图片实时转换消耗浏览器性能,在网站上观察转换为PNG/JPEG有几百毫秒不等的延时;库很大,minify之后仍然有69KB;无法处理HTML/CSS中的图片引用。

以上几个方案对HTML4, CSS, JS几个入口WebP图片引用的支持情况如下:

方案 类型 HTML引用 CSS引用 JS引用
picture 拦截器
客户端拦截 拦截器
资源加载拦截 拦截器
多版本代码 拦截器
libwebpjs 解析器

在iOS,Android客户端(Webview)和浏览器运行环境中兼容情况如下(✅使用WebP,❌不使用WebP,⚠️部分用WebP,❗️运行可能出错):

方案 iOS客户端 Android客户端 iOS浏览器 Android浏览器
picture ❗️⚠️部分浏览器 ❗️⚠️部分浏览器 ❗️⚠️部分浏览器 ❗️⚠️部分浏览器
客户端拦截 ✅需要拦截处理 ❗️ ❗️⚠️部分浏览器
资源加载拦截 ⚠️JS ⚠️JS ⚠️JS ⚠️JS
多版本代码 ⚠️部分浏览器
libwebpjs ⚠️JS ⚠️JS ⚠️JS ⚠️JS

从表格可以看出,对于常规的HTML5项目来说:

  1. <picture>:排除,兼容性不太好,目前阶段还不适用;
  2. 客户端拦截:排除,因为在浏览器环境下运行可能不正常;
  3. 资源加载拦截:排除,不能处理HTML和CSS的图片引用,JS里也要单独写加载器;
  4. 多版本代码:相比之下较为理想,能够自动处理HTML, CSS, JS中的图片引用,运行兼容性也能保证,唯一的缺点是对于不支持WebP的浏览器得fallback回到PNG图片。
  5. libwebpjs:排除,缺点太多,库大、转码耗性能、不能处理HTML和CSS的图片引用。

多版本代码解决方案的实现

我用Node.js + React + Webpack + CSS Modules等技术写了个demo项目,源代码见https://github.com/swenyang/webp-multi-versioning

主要思路如下:

  1. 用Node.js写build脚本,从Webpack配置编译出不用WebP和用WebP的两种版本代码。
  2. HTML引用Webpack生成的js脚本时,不直接用<script src="xxx">引入,先判断浏览器的WebP能力,再对应载入WebP版本的代码或者非WebP版本的代码:

     var isWebPSupported;  
     (function () {  
         var WebP = new Image();  
         WebP.onload = WebP.onerror = function () {  
             isWebPSupported = WebP.height === 2;  
    
             var name = isWebPSupported ? 'main.f041d9036d33197bd734.js' : 'main.9f0e87ed2db271e1a42b.js';  
             var s = document.createElement('script');  
             s.src = name;  
             document.body.appendChild(s);  
    
         };  
         WebP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';  
     })();
    

在线的demo请见https://swenyang.github.io/webp-multi-versioning/

总结

虽然WebP诞生至今已经接近7个年头,但是浏览器支持一直局限于Google自家出的Chrome和Android浏览器、Opera浏览器,其他系列鲜有支持。这个背景下,加上HTML5的特殊性,WebP解决方案的优势并不非常明显,最好结合所在团队和业务的情况因地制宜。

本文给出了5个解决方案,每个方案有各自的优劣势,总结适用场景分别如下:

  1. <picture>:适用于不太考虑兼容低版本Android和iOS的项目(前文讨论分别有约50%和18%,比例不小),方案实现容易;
  2. 客户端拦截:适用于只运行在客户端环境内的项目(Hybrid App),方案实现容易;
  3. 资源加载拦截:适用于只从JS代码加载图片的项目,目前来看比较偏向于游戏场景,方案实现比较容易;
  4. 多版本代码:适用于大部分项目,兼容性好,但方案实现比较难;
  5. libwebjs:适用于不太考虑性能、且只从JS代码加载图片的项目(感觉除了demo外没有特别好的使用场景)。

对于多版本代码方案,写了一个demo实现了自动构建,如果考虑使用WebP可以参考借鉴一下,项目地址是https://github.com/swenyang/webp-multi-versioning,欢迎指正。

1. WebP对比PNG/JPEG格式图片的数据来源于Google WebP官方介绍https://developers.google.com/speed/webp/
2. 本文讨论的HTML5项目(也可称前端项目)是指既可能运行在浏览器,也可能运行在iOS/Android App的Webview中的HTML5代码,包括HTML/CSS/JS等。
3. 假设PNG24 -> PNG8只有色彩损失,其他品质不变,所以使用q=100
4. React的JSX语法看做是HTML的一部分。

results matching ""

    No results matching ""