webp渐进接入

197 阅读5分钟

1. webp相比png

无损压缩后的 WebP 比 PNG 文件少了 45% 的文件大小,即使这些 PNG 文件经过其他压缩工具压缩之后,WebP 还是可以减少 28% 的文件大小。来一张图对比下。

png: xxx.png      webp: xxx.webp

可以看到使用webp图片后,使用图片的体积下降了85/100左右

2. 接入方案

1.腾讯云提供的方案

0001000224100089731c4eaeb1400102?height=920&width=1920
腾讯云提供的两种解决方案。

第一种是对于域名开启请求头部自适应,在客户端请求发起的时候,根据请求头部的accept字段来支持。 举个例子,accept: image/png;image/webp;image/avif。服务端在收到请求后,会降级返回图片。先返回avif,再返回webp,最后支持png。 这种方案适合是无需客户端改造的。但是在一些场景下不适用,比如说已有的业务怎么验证、客户端如果错误携带返回头等参考桌面端macos的safari。
第二种是万象数据的解决方案,域名提供不同格式的图片,业务自行判断。比如url为 xxx.png。 业务需要webp,使用格式为xxx.png?imageMogr2/format/webp。这种方案有对业务有一定改造工作量,但是自由程度较高,且影响范围为自己的单一业务,不会影响到域名下的其他业务。

综合业务的类型,这里使用第二种。需要注意成本问题,万象数据调用内部价格为6块/1k次。需要cdn缓存支持到。

2. 业界方案

       1. 使用一段内联的webp来加载和渲染。

function check_webp_feature(feature, callback) {
    var kTestImages = {
        lossy: "UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA",
        lossless: "UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==",
        alpha: "UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAARBxAR/Q9ERP8DAABWUDggGAAAABQBAJ0BKgEAAQAAAP4AAA3AAP7mtQAAAA==",
        animation: "UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA"
    };
    var img = new Image();
    img.onload = function () {
        var result = (img.width > 0) && (img.height > 0);
        callback(feature, result);
    };
    img.onerror = function () {
        callback(feature, false);
    };
    img.src = "data:image/webp;base64," + kTestImages[feature];
}

 这里可以看到是去下载和正常渲染webp。如果ok就认为是支持webp的。但是对于背景图,目前只有替换class的办法,对于新业务和已有业务的开发是很繁琐的。改动也是很容易出错的。

3. 我们的前端业务改造方案

       1.   通过caniuse查询出webp的支持度如下,浏览器支持度为95/100左右。现代浏览器都几乎支持,除了safari对于这一块的支持度较晚,桌面平台为16开始支持,移动端为14开始支持。

             00010002241000009d8d5184024b8701?height=736&width=2706 

这里我们采取的方案借鉴现代浏览器的特性,站在巨人的肩膀上,对webp做一个渐进的支持。

  1. 使用@support媒体查询对webp做渐进支持。我们只需要选择和webp支持相近,一定支持webp的css属性就好了。不支持媒体查询的不会用到webp图片,很安全。测试在ie浏览器桶都支持。

000100022410001689aff3a63549ff02?height=994&width=2780可以看到@supports对css的支持是很好的,比webp还高。

代码如下,我们选择了contain和 text-align-last属性。支持度为chrome53 edge79 safari16 firfox69 opera34,是webp的子集,可以放心使用。 对于移动端的ios设备,希望支持到14左右。这里选用了translate属性和-webit-touch-callout属性支持到14.5。通过联合查询可以看到对css的支持度达到92/100和93/100,95/100 和 90+ 差距不大。

// !!!url不能随意修改,会产生费用
@mixin webpimgurl($url) {
  $webp: $url + '?imageMogr2/format/webp/ignore-error/1';
  $png: $url;

  background-image: url(#{$png});
  // 大多数浏览器和ios16的safari
  @supports (contain: none) and (text-align-last: auto) {
    background-image: url('#{$webp}');
  }

  // safari 移动端14.5起支持
  @supports (translate: 10px) and (-webkit-touch-callout: none) {
    background-image: url('#{$webp}');
  }
}
3. 使用CSS.supports来判断就好,原理和兼容度同CSS的媒体查询。
      // opera
      var supports = window.supportsCSS;
      // not ie
      if (window.CSS && window.CSS.supports) {
        supports = window.CSS.supports;
      }
      if (supports) {
        var supportContain = supports('contain', 'none');
        var supportText = supports('text-align-last', 'auto');
        var supportTranslate = supports('translate', '10px');
        var supportTouchCallout = supports('-webkit-touch-callout', 'none');
        var supportModeCSS = supportContain && supportText;
        var supportModeIOS = supportTranslate && supportTouchCallout;
        window.supportWebp = supportModeCSS || supportModeIOS;
        console.log('supportWebp2', supportModeCSS, supportModeIOS, window.supportWebp);
      }
4. 对于小程序的webp支持。由于小程序对版本获取很方便,这里使用对应接口判断版本即可。对于image的webp属性,是微信小程序自带的解码器,会对webp图片在低版本时处理。
export function supportWebp() {
  if (!wx.getDeviceInfo || !wx.getAppBaseInfo) {
    return false;
  }
  const data = wx.getDeviceInfo?.();
  const sdkData = wx.getAppBaseInfo?.();
  wx.log.info('getSystemInfoSync', data);
  const system = data.system?.toLocaleLowerCase?.();
  const sdk = sdkData.SDKVersion?.toLocaleLowerCase();
  const { platform } = data;

  wx.log.info('supportWebp', platform?.toLowerCase?.());

  if (platform?.trim?.().toLowerCase?.() === 'devtools') {
    return true;
  }

  // ios系统16有bug,暂时设置为false
  if (platform?.trim?.().toLowerCase?.() === 'mac') {
    return false;
  }

  if (platform?.trim?.().toLowerCase?.() === 'windows') {
    return true;
  }

  if (system?.indexOf('ios') !== -1) {
    const version = system.toLocaleLowerCase?.()?.replace?.('ios', '').trim?.();
    const versionArray = version?.split('.');
    let iosVersionMatch = false;
    if (versionArray.length) {
      wx.log.info('versionArray', versionArray);
      const mainVersion = versionArray[0];
      const mainVersionNumber = parseInt(mainVersion);
      iosVersionMatch = mainVersionNumber >= 14;
    }

    wx.log.info('iosVersionMatch', iosVersionMatch);

    if (iosVersionMatch) {
      wx.report?.('app_webp_support', {
        support: 1,
        platform: 2,
      });
      return true;
    }

    wx.report?.('app_webp_support', {
      support: 0,
      platform: 2,
    });

    return false;
  } else {
    if (system?.indexOf('android') !== -1) {
      wx.report?.('app_webp_support', {
        support: 1,
        platform: 1,
      });
      return true;
    }
  }
  wx.report?.('app_webp_support', {
    support: 0,
    platform: 3,
  });
  return false;
}

 在wxml中对图片使用wxs作为过滤器即可。web_support是全局注入的变量。

  <image class="item-img"
         src="{{ webpTools.parseImg(webp_support, 'xxxxxxx.png') }}">

 这里的过滤器对图片处理需要限制域名和对图片后缀限制png/jpg/jpeg等情况处理。

function getUrlBase(url) {
  // 使用正则表达式提取协议、域名和路径
  var protocol = url.substring(0, url.indexOf("://"));
  // 获取域名部分
  var domainStartIndex = url.indexOf("://") + 3;
  var domainEndIndex = url.indexOf("/", domainStartIndex);
  var domain = url.substring(domainStartIndex, domainEndIndex);
  // 获取路径部分
  var path = url.substring(domainEndIndex);
  var newUrl = protocol + '://' + domain + path;
  var length = url.length;
  var fileExtName = url.substring(length-4);
  var protocol = url.substring(0,5);
  return {
    newUrl: newUrl,
    domain: domain,
    fileExtName: fileExtName,
    protocol: protocol,
  };
}

function parseImg(supportWebp, url) {
  if (!url) {
    return url;
  }
  if (typeof url !== 'string') {
    return url;
  }
  var newUrlIndex = url.indexOf('?');
  if (newUrlIndex === -1) {
    var data = getUrlBase(url);
    var newUrl = data.newUrl;
    var domain = data.domain;
    var fileExtName = data.fileExtName;
    var protocol = data.protocol;
    console.log('parseImg fileExtName protocol', protocol);
    if (protocol !== 'https') {
      console.log('parseImg fileExtName protocol', protocol);
      return url;
    }
    if (fileExtName !== '.jpg' && fileExtName !== '.JPG' && fileExtName !== '.png' && fileExtName !=='.PNG' && fileExtName !== '.jpeg' && fileExtName !== '.JPEG' ) {
      console.log('parseImg no support', fileExtName);
      return url;
    }
    if (domain !== 'xxx.com' && domain === 'xxxx.qq.com') {
      console.log('parseImg no support', domain);
      return url;
    }
    // !!!url不能随意修改,会产生费用
    if (domain && supportWebp) {
      return newUrl+ '?imageView2/format/webp/ignore-error/1';
    }
    console.log('parseImg  not support', domain, supportWebp);
    return url;
  }
  console.log('parseImg not support webp', url);
  // 有query不处理
  return url;
}

module.exports = {
 parseImg: parseImg,
};