林夕轩

立志于前端的超级菜鸟就是我

美团在webp方面的实践

为什么推广 WebP

更省流量

Google Developers 中提到使用 WebP 能够比 jpg 节省30%+的流量。

用户基数大

在上述文档中也提到支持 WebP 有如下的浏览器:

  • Google Chrome (desktop) 17+
  • Google Chrome for Android version 25+
  • Opera 11.10+
  • Native web browser, Android 4.0+ (ICS)*

有 1/3 的美团用户使用 chrome 17+ 浏览器,如果为他们展示 WebP 图片,预期将为这些用户节省 1/3 左右的流量。而且这个比例还在随时间增加!

结论

综上,在美团推广 WebP 是可行且必要的。

如何推广 WebP

技术基础

目前,图片服务器支持压缩 WebP 格式的图片,但只提供压缩服务,并不会把每种尺寸/格式的图片存起来,而交给CDN进行缓存。(这会引发一个在服务端评估效果的问题,后面会提到。)

方案实施分为两个部分:

  • 检测方案:负责识别用户是否可使用 WebP 服务
  • 切换方案:负责给用户输出 WebP 图片或者 jpg 图片

检测方案

在检测方案方面,我们有两种选择:黑名单使用 JavaScript 检测。接下来分别详细介绍。

黑名单

顾名思义,假设我们维护一个黑名单,当检测到用户的 UA 在黑名单中,则使用 jpg 格式的图片,其它的浏览器则使用 webp 格式的图片。

caniuse 中有不同浏览器对 WebP 的兼容性介绍。

JavaScript 检测方案

stack-overflow detecting-webp-support 上有个一个非常值得借鉴的逻辑,原理就是请求一个 WebP 格式的图片。因为图片在加载的时候会触发 onload 或者 onerror 事件。当浏览器成功加载该图片的时候会触发 onload 事件,否则会触发 oneror 的事件。所以,我们会先将检测结果保存起来,在用户下一次访问页面的时候提供对应格式的图片。

方案比较

方案 优势 缺陷
黑名单 可在服务端进行切换;实现难度低 不可扩展;几乎不可测试;严重依赖黑白名单的正确性,不靠谱;使用CDN后,用户的UA不能方便获得
JavaScript 检测 比较靠谱,能力判断优于类型判断; 需要使用 Cookie 存储数据;增加一个网络请求;增加开发难度

经过对比,虽然方案二有如上3个缺陷,但相比不可扩展、不可测试、不靠谱带来的不稳定,这些都是可以接受的。经过讨论,我们最终选择了使用 JavaScript 检测的方案。然而针对上面提到的增加网络请求、使用 Cookie 等问题,我们在实施过程中也逐渐解决了!

于是我们有了如下第一个版本的检测代码:

function hasWebp () {
    // 查看 Cookie,如果没有则进行以下逻辑
    var img = new Image();
    img.onload = handleSupport;
    img.onerror = handleNotSupport;
    img.src = 'http://www.gstatic.com/webp/gallery/1.webp';
    // 否则根据 Cookie 执行handleSupport或者handleNotSupport
}
function handleSupport() {
    setCookie('swebp', 'true', 2592000); // 30天过期
}
function handleNotSupport () {
    setCookie('swebp', 'false', 2592000);
}

请求一个外网的 WebP 图片,成功后,给 Cookie 增加一个可行的字段,否则增加一个不可行字段。这样就会带来上面方案比较中提到的两个缺点:请求一个外网的图片,就会新增一个网络请求; Cookie 是一个稀缺资源,仅仅为了增强体验而影响正常功能是得不偿失的。于是我们有了优化方案如下:

使用base64,减少一个图片请求

因为 base64 的特性,我们可以在不用发出图片请求的情况下加载一张图片。而我们的做法就是将一个 1x1 大小的 WebP 格式图片压缩成 base64 格式,然后方案如下:

function hasWebp () {
    // 查看Cookie,如果没有则进行以下逻辑
    var img = new Image();
    img.onload = handleSupport;
    img.onerror = handleNotSupport;
    img.src = 'data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAsAAAABBxAREYiI/gcAAABWUDggGAAAADABAJ0BKgEAAQABABwlpAADcAD+/gbQAA==';
    // 否则根据Cookie执行handleSupport或者handleNotSupport
}
function handleSupport() {
    setCookie('swebp', 'true', 2592000); // 30天过期
}
function handleNotSupport () {
    setCookie('swebp', 'false', 2592000);
}

在 src 中声明请求的图片格式为 WebP 格式,就能请求指定的 WebP 图片了。

不使用 Cookie 而保存用户信息的方法,大家肯定都想到 localStorage 了。我们转而使用 localStorage 的理由是:支持 WebP 的浏览器都是高级浏览器,它们一定都支持 localStorage。于是我们得到第三版的检测代码如下:

function detectWebp () {
    if (!window.localStorage || typeof localStorage !== 'object') return;

    var name = 'webpa'; // webp available
    if (!localStorage.getItem(name) || (localStorage.getItem(name) !== 'available' && localStorage.getItem(name) !== 'disable')) {

        var img = document.createElement('img');

        img.onload = function () {
            try {
                localStorage.setItem(name, 'available');
            } catch (ex) {
            }
        };

        img.onerror = function () {
        try {
                localStorage.setItem(name, 'disable');
            } catch (ex) {
            }
        };
        img.src = 'data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAsAAAABBxAREYiI/gcAAABWUDggGAAAADABAJ0BKgEAAQABABwlpAADcAD+/gbQAA==';
    }
}

更优的方案

使用 loacalstorage 会带来另一个问题:美团有很多子域,当用户切换城市后,每个子域下都会保存一个 localStorage 的记录,从而浪费空间。本组同学实现了一个跨域 localStorage 的功能:通过通信机制实现将检测结果保存在www子域中。实现的原理是通过在多个子域中的页面中插入www 子域下的 iframe。

使用这种方案必须通过回调的方式获取信息,且需要等到 iframe 加载完全。但切换图片图片是在非常前期进行的,如果依赖这种方案,会导致所有图片的渲染比较靠后进行。

回过头来看,其实美团是提供本地服务的,频繁切换城市的情况非常少。所以,使用原生的 localStorage 是可行的。

为了实现为了能给浏览器种下 WebP 支持能力的“种子”,在检测代码上线两天后再上线替换逻辑。

切换方案

在切换图片 URL 的时候,我们选择使用 JavaScript 而不是服务端,主要是因为有两个原因:

  • 页面静态化:目前有些页面已经静态化了,如果当用户使用支持 WebP 格式的浏览器访问美团,恰巧服务端把这段信息缓存下来。短时间内其他用户使用不支持 WebP 的浏览器访问美团,则无法显示图片。
  • 使用 CDN 缓存图片资源:因为使用 CDN 缓存了图片资源,用户的图片请求将被 CDN 拦截,服务端不方便获取用户的 UA 信息。

切换逻辑

之前提到,图片服务器已经提供了 WebP 格式图片,只需请求带有 .webp 链接的图片即可。切换 WebP 的原理很简单,就是给 img 的 src 属性切换不同的链接即可。

因为美团中85%以上的图片均是懒加载的,这就不依赖服务器来替换图片 URL 。我们只需要在 JavaScript 替换图片 URL 的时候,选择是否在 URL 后面加上'.webp'

于是,我们只是在懒加载函数中增加一个如下切换逻辑即可:

_getwebpsrc: function (ndimg, imgsrc) {
    var needwebp = false,
        src = '';
    if (window.localStorage && typeof localStorage === 'object') {
        needwebp = localStorage.getitem('webpa') === 'available';
    }
    src = needwebp ? imgsrc + '.webp' : imgsrc;

    return src;
}

懒加载的功能是在合适的时机设置 img 的 src 属性,而只要在替换之前,使用如上逻辑设置要替换的值即可。

上线方案

具体在上线 WebP 的时候,我们给测试用户的页面中的图片增加"J-webp"属性,而在替换逻辑中也只替换带有 J-webp 属性的图片。替换逻辑改成如下:

_getwebpsrc: function (ndimg, imgsrc) {
    var webp_class = 'J-webp',
        needwebp = false,
        src = '';
    if (window.localStorage && typeof localStorage === 'object') {
        needwebp = localStorage.getitem('webpa') === 'available' && ndimg.hasClass(webp_class);
    }
    src = needwebp ? imgsrc + '.webp' : imgsrc;

    return src;
}

评估方案

在上线了 WebP 之后,需要进行流量分析效果。比较所有被请求的图片使用 WebP 格式,相比于 jpg 格式节省多少流量。评估最好以两个维度:

  • 客户端:从客户的角度,访问一个页面,或者请求N张图,一共节省了多少流量。
  • 服务端:从公司角度,在一段时间(N次请求)内,节省了多少带宽。

客户端方向

使用浏览器或者简单爬虫获取一个页面,比较一个页面中的全部图片,使用 WebP 节省的流量。

浏览器方案

寻找一个浏览器,能够使用 JavaScript 获取该页面内所有图片的 imgSize。经过调研发现,目前只有 IE 浏览器支持使用 JavaScript 获取图片大小:

var sizeSum = 0;
Y.all('.deal-tile').each(function (node) {
    sizeSum += parseInt(node.one('img').getDOMNode().fileSize, 10);
});

alert(sizeSum/1024);

在 IE 控制台使用如上脚本就能获取非 WebP 图片的总体积。但是,因为 IE 不支持 WebP ,所以无法得出测试组的大小。

然而,在 Chrome 等支持 WebP 的浏览器却无法获取图片大小。于是,单独使用浏览器是无法计算一个页面使用 WebP 能减少多少流量的。所以,此方案否决

爬虫脚本方案

思路就是请求一个页面,匹配出所有的图片链接。假设有N张图片。请求 N 张 WebP 格式的图片和 N 张 jpg 格式的图片,计算总大小的差值。

请求图片体积的 PHP 脚本如下:

function getMTImageSize($url)
{
    $handle = fopen($url, 'rb');
    if (!$handle) return false;

   $meta = stream_get_meta_data($handle);
   $dataInfo = isset($meta['wrapper_data']['headers']) ? $meta['wrapper_data']['headers'] : $meta['wrapper_data'];

    foreach($dataInfo as $va) {
       if (preg_match('/length/iU', $va)) {
            $ts = explode(':', $va);
            $result['size'] = trim(array_pop($ts));
            break;
        }
    }
    fclose($handle);

    return $result;
}

先检验一下爬虫脚本得到的数据是否可靠。在测试的时候,首页的第二张图片链接为:http://p0.meituan.net/320.0.a/deal/__40558212__3778718.jpg

IE浏览器中执行如下代码:

alert(Y.all('.J-webp').item(1).getDOMNode().fileSize);
// 执行结果 "56042"

PHP脚本执行结果:

$result = getMTImageSize('http://p0.meituan.net/320.0.a/deal/__40558212__3778718.jpg');
print_r($result);

[size] => 56042

可以从上面的结果看到,两种方式的数据相同。应该可以认为PHP脚本的数据是靠谱的。

编写一个简单的爬虫脚本:

// 使用函数获取所有data-src属性的内容,即将被替换的图片链接
function getMTFloor($url, $needWebp = false)
{
    $handle = fopen($url, 'rb');

    $lines_string = "";
    do{
        $data=fread($handle,1024);
        if(strlen($data)==0) {
            break;
        }
        $lines_string.=$data;
    }while(true);

    fclose($handle);

    $matches = array();

    preg_match_all('/data-src="(\S+)"/', $lines_string, $matches);

    $sizeSum = 0;
    $index = 0;

    foreach($matches[1] as $url) {
        if ($needWebp) {
           $url .= '.webp';
        }
        $sizeSum += (int)(getMTImageSize($url));
        echo ($index++);
    }

    var_dump($sizeSum);
}
getMTFloor('http://bj.meituan.com');
getMTFloor('http://bj.meituan.com', true);

得出的结果是使用 WebP 的总流量为:1632932bit,而不使用 WebP 的流量为:3266405bit。通过对着一个页面的分析,对于需要懒加载的88张图片中,如果都使用 WebP 能够节省近一半的流量。

从结果来看,使用 WebP 节省的流量是符合预期的。但因样本量受随机性的影响,于是我们在爬虫脚本上做了一点拓展:

public static function getFullSizeImg() {
    foreach($citys as $city) {
        $url = 'http://'. $city. '.meituan.com/';
        self::getMTFloor($url, false);
        self::getMTFloor($url, true);
    }

    $res = '<?php
        $jpg = ['. implode(self::$jpg, ','). '];
        $webp = ['. implode(self::$webp, ','). ']';
    file_put_contents('webpData.php', $res);
}

爬取美团300个分站点的首页,平均每个首页有70张图片,分别请求约21000张两种格式的图片,计算每张图片的差值的平均值。考虑到不同分站点的首页图片数不尽相同,最终得到的样本数为18783。对测试组 webp 和对照组 jpg 利用公式:

ratio[i] = (jpg[i] - webp[i]) / jpg[i];
sum(ratio)/count(ratio);

得出平均值约为43%。

我们从客户端的角度来看,使用 WebP 是能够达到甚至超过预期效果的。

服务端角度

方案一:

服务器保存两组图片—— jpg 格式和 WebP 格式。它们总大小比例为 ratio,而假设一天每张图片平均访问量为N。则可以预估使用 WebP 节省的流量为(1 - ratio) * N;因为前面提到,图片服务器不保存图片,只提供压缩服务。该方案被否决.

方案二:

根据脚本计算的使用 WebP 平均节省流量为43%,然后统计一天内 WebP 压缩 API 的次数 M ,而假设每张图片的平均大小为V。则可以得出一天内使用 WebP 能够节省的流量约为:43% M V 。但是,因为使用 CDN ,对同一个 URL 图片请求只有第一次会调用压缩 API ,其它的请求都会被挡在 CDN。所以这个方案被否决

虽然我们没能在服务端得到具体的统计数据,但通过客户端获取的压缩比可以证明,当图片的请求次数越多,对服务端的流量的节省越显著。

结论

经过方案实施到效果评估,我们基本可以得出结论在使用 WebP 的情况下,是能节省30% ~ 40%的图片流量的。这意味着在相同的网速下,用户在较少的时间内就能加载好相同品质的图片。

参考文献

2014-10-20

webp 性能