站點優化之 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。

相關文章
相關標籤/搜索