站点优化之 WebP 实战

webp

✏️最新内容请以github上的为准❗️

其余文章

在搭建 blog 过程当中,使用 lighthouse 审查站点。在性能项上提示Serve images in next-gen formats优化建议。html

Image formats like JPEG 2000, JPEG XR, and webp often provide better compression than PNG or JPEG, which means faster downloads and less data consumption.Learn morenode

JPEG 2000, JPEG XR, 和 WebP 与传统的 JPEG、PNG 相比具备高压缩比、高质量的特色。这让图片加载更快,带宽消耗更少。当前浏览器对 JPEG 2000, JPEG XR, 和 WebP 的支持状况:nginx

  • WebP:Chrome、Oprea、UC、QQ。其中 Firefox 新版已支持,Safari 已开始尝试支持。
  • JPEG 2000:Safari
  • JPEG XR:IE

结合浏览器的支持状况,最终选择支持 WebP 来优化:git

  • 支持有损和无损压缩
  • 支持动画
  • 开源
  • 技术支持团队是 Google
  • 更多关于 WebP

如何支持 WebP

支持 WebP 有两种方式:github

  1. 客户端处理,这种处理方式须要提早准备好 WebP 图片。如何将图片转换为 WebP 格式web

    • 使用 js 检测是否支持 WebP。
    // check_webp_feature:
    // 'feature' can be one of 'lossy', 'lossless', 'alpha' or 'animation'.
    // 'callback(feature, result)' will be passed back the detection result (in an asynchronous way!)
    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];
    }
    复制代码
    • 使用 HTML5 和 CSS3 特性支持检测库: Modernizr 。Modernizr.webp,Modernizr.webp.lossless,Modernizr.webp.alpha 和 Modernizr.webp.animation。
    • 使用 <picture> 元素
    <picture>
        <source type="image/webp" srcset="demo.webp">
        <source type="image/png" media="demo.png">
        <img src="demo.png" alt="demo">
    </picture>
    复制代码
  2. 服务端处理。相比客户端处理,在服务端处理更加灵活。由于它能够经过内容类型协商,能提早知道客户端是否支持 WebP(请求头中Accept字段)。若是支持就优先响应 Web 格式图片,不然就响应请求图片。apache

对比两种处理方式,经过服务端来支持 WebP 具备以下优点:npm

  • 提早知道客户端是否支持 WebP。处理更灵活,更可靠。而客户端还须要根据是否支持 WebP,对连接作额外的替换处理。
  • 动态支持 WebP。若是支持 WebP,查看本地是否有对应 WebP 图片,若是没有动态生成响应。

服务端动态支持 WebP

服务端要动态支持 WebP,能够由代理服务器 Nginx,或 Backend 来完成。浏览器

singsong:图片处理逻辑最好交给下游 Backend 来完成,NGINX 就负责转发便可。固然也有自动处理图片 nginx :ngx_pagespeedbash

Nginx 处理

  1. 确保mime.types中有 WebP。由于若是没有 WebP 类型,WebP 图片会做为application/octet-stream 输出。
image/webp  webp;
复制代码
  1. 获取请求头 Accept 字段中的 webp
map $http_accept $webp_suffix {
  default   "";
  "~*webp"  ".webp";
}
复制代码

这里使用 map(更多参考ngx_http_map_module)定义了一个$webp_suffix变量,若是 WebP 存在,$webp_suffix值为".webp",不然为空字符串。

  1. 输出图片

    • 查看是否存在.webp的文件,若是存在就直接输出。
    • 查看是否存在请求文件,若是存在就直接输出。
    • 若是上述文件都不存在,就响应404
try_files $uri$webp_suffix $uri =404;
复制代码

这里还能够将响应操做反代理给 Backend:

if ($http_accept ~* "webp")    { set $webp_accept "true"; }

    location ~ ^/imgs.*\.(png|jpe?g)$ {
      # Pass WebP support header to backend
      proxy_set_header  WebP  $webp_accept;
      proxy_pass http://127.0.0.1:8080;
    }
复制代码

完整代码:

worker_processes 1;

events {
  worker_connections 1024;
}

http {
  include mime.types;
  default_type  application/octet-stream;

  #
  # < regular Nginx configuration here >
  #

  # For a hands-on explanation of using Accept negotiation, see:
  # http://www.igvita.com/2013/05/01/deploying-webp-via-accept-content-negotiation/

  # For an explanation of how to use maps for that, see:
  # http://www.lazutkin.com/blog/2014/02/23/serve-files-with-nginx-conditionally/

  map $http_accept $webp_suffix {
    "~*webp"  ".webp";
  }
  map $msie $cache_control {
      "1"     "private";
  }
  map $msie $vary_header {
      default "Accept";
      "1"     "";
  }

  # if proxying to another backend and using nginx as cache
  proxy_cache_path  /tmp/cache levels=1:2 keys_zone=my-cache:8m max_size=1000m inactive=600m;
  proxy_temp_path /tmp/cache/tmp;

  server {
    listen       8081;
    server_name  localhost;

    location ~ \.(png|jpe?g)$ {
      # set response headers specially treating MSIE
      add_header Vary $vary_header;
      add_header Cache-Control $cache_control;
      # now serve our images
      try_files $uri$webp_suffix $uri =404;
    }

    # if proxying to another backend and using nginx as cache
    if ($http_accept ~* "webp")    { set $webp_accept "true"; }
    proxy_cache_key $scheme$proxy_host$request_uri$webp_local$webp_accept;

    location ~ ^/proxy.*\.(png|jpe?g)$ {
      # Pass WebP support header to backend
      proxy_set_header  WebP  $webp_accept;
      proxy_pass http://127.0.0.1:8080;
      proxy_cache my-cache;
    }
  }
}
复制代码

想了解更多能够参考以下文章:

Backend 处理

Backend 是基于 KOA 框架搭建的,要集成动态支持 WebP,须要完成以下两个任务:

  • 获取请求头中的Accept字段,判断是否支持 WebP。这一步也可由 Nginx 来作。
// 获取请求头:ctx.header.accept, ctx.headers.accept、ctx.req.headers.accept、ctx.request.headers.accept、ctx.request.header.accept
const isWebp = /webp/i.test(ctx.header.accept);
// 注意: 虽然 KOA 提供`ctx.accept('webp')`方法来判断accept type。可是该方法对webp判断存在bug,它会将`*/*`做为支持来处理。
复制代码
  • 添加图片处理功能。要动态支持 WebP,这就须要 Backend 具有图片处理功能。node 相关的图片处理库:

sharp 相比于 jimp、gm 综合性能更好,对 WebP 支持更友好。所以这里使用 sharp 来实现图片格式转换、缩放、水印等功能。npm 对比数据:gm vs jimp vs sharp

关键代码

const fs = require("fs-extra");
const path = require("path");
const send = require("koa-send");
const sharp = require("sharp");
const glob = require("glob");
const TextToSvg = require("text-to-svg");

// 配置sharp
sharp.concurrency(1);
sharp.cache(50);
module.exports = async ctx => {
  // getSvgByText
  const getSvgByText = (text, fontSize, color) => {
    const textToSVG = TextToSvg.loadSync();
    const svg = textToSVG.getSVG(text, {
      fontSize,
      anchor: "top",
      attributes: {
        fill: color
      }
    });
    return Buffer.from(svg);
  };

  const originals = glob.sync(
    path.join(__dirname, "public", "originals", "*.+(png|jpeg|svg|jpg)")
  );
  
  const nameMapOriginal = {};
  originals.forEach(original => {
    const metas = path.parse(original);
    nameMapOriginal[metas.name] = original;
  });

  // getOriginals
  const getOriginalsByName = name => nameMapOriginal[name];

  const imgProcessor = async (
    inputPath,
    outputPath,
    { overlay, width, blur }
  ) => {
    const image = sharp(inputPath);
    const metadata = await image.clone().metadata(); // 获取原图片的元数据
    const rawWidth = width || metadata.width;

    if (
      overlay !== "off" &&
      metadata.width > 200 &&
      metadata.height > 100 &&
      rawWidth > 200
    ) {
      const tempFontSize = (rawWidth * 0.03) | 0; // eslint-disable-line
      const fontSize = tempFontSize < 12 ? 12 : tempFontSize;
      overlay = getSvgByText(
        "zhansingsong.com",
        fontSize,
        "rgba(255, 255, 255, 0.3)"
      ); // eslint-disable-line
      await image
        .clone()
        .overlayWith(overlay, { gravity: sharp.gravity.southeast })
        .resize({ width: parseInt(width, 10) })
        .toFile(outputPath)
        .catch(err => ctx.app.emit("error", err));
    } else if (!blur) {
      await image
        .clone()
        .resize({ width: parseInt(width, 10) })
        .toFile(outputPath)
        .catch(err => ctx.app.emit("error", err));
    } else {
      await image
        .clone()
        .resize({ width: parseInt(width, 10) })
        .blur(1.3)
        .toFile(outputPath)
        .catch(err => ctx.app.emit("error", err));
    }
  };
  const { join, parse } = path;
  const { existsSync, ensureDirSync } = fs;
  // 编码中文乱码
  const url = decodeURIComponent(ctx.path);
  const metas = parse(url);
  const isWebp = /webp/i.test(ctx.header.accept); // 判断是否支持webp
  const isThumbnail = /^\/public\/thumbnails\//.test(url);
  const fileDir = isThumbnail
    ? join.apply(path, [
        __dirname,
        "public",
        "thumbnails",
        `${ctx.query.width || 20}`
      ])
    : join.apply(path, [
        __dirname,
        "public",
        "imgs",
        ...Object.values(ctx.query)
      ]);
  const filePath = join(
    fileDir,
    `${metas.name}${isWebp ? ".webp" : metas.ext}`
  );
  const options = isThumbnail
    ? {
        width: ctx.query.width || 20,
        overlay: ctx.query.overlay || "off",
        blur: true
      }
    : ctx.query;

  ensureDirSync(fileDir);
  if (!existsSync(filePath)) {
    await imgProcessor(getOriginalsByName(metas.name), filePath, options); // eslint-disable-line
  }
  await send(ctx, filePath, { root: "/" });
};
复制代码

实现效果

经过 sharp 为 Backend 实现了一些简单图片处理接口:图片压缩、水印、格式转换。这也为后面缩略图的使用提供了支持。处理效果以下图所示:

webp-support
从上图可知:

  • Safari 和 Chrome 浏览器分别请求同一图片,响应结果各不相同。浏览器支持 WebP 时,会直接响应 WebP 图片。不然就响应请求图片。
  • 相同质量的图片,WebP 格式大小约为 png 格式大小的 0.43。

总结

本文是本身在使用 WebP 的一些心得总结。主要对 WebP 的使用作个简单介绍。至于为何要用 WebP,本文也作了相关介绍。但这并不表明 WebP 没有缺点。如在编解码效率上就存在不足。不过随着硬件设备的提高,这也在可接受范围内。随着移动互联网的快速发展,PWA(Progressive Web App)必成为 Web App 的主流。而 WebP 是 PWA 一个组成部分,了解并支持 WebP 已成大趋势。目前不少主流的站点已全站或部分支持 WebP。

相关文章
相关标签/搜索