站點優化之 WebP 實戰

singsong發表於2019-02-28

webp

✏️最新內容請以github上的為準❗️

其他文章

在搭建 blog 過程中,使用 lighthouse 審查站點。在效能項上提示Serve images in next-gen formats優化建議。

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 more

JPEG 2000, JPEG XR, 和 WebP 與傳統的 JPEG、PNG 相比具有高壓縮比、高質量的特點。這讓圖片載入更快,頻寬消耗更少。當前瀏覽器對 JPEG 2000, JPEG XR, 和 WebP 的支援情況:

  • WebP:Chrome、Oprea、UC、QQ。其中 Firefox 新版已支援,Safari 已開始嘗試支援。
  • JPEG 2000:Safari
  • JPEG XR:IE

結合瀏覽器的支援情況,最終選擇支援 WebP 來優化:

  • 支援有損和無失真壓縮
  • 支援動畫
  • 開源
  • 技術支援團隊是 Google
  • 更多關於 WebP

如何支援 WebP

支援 WebP 有兩種方式:

  1. 客戶端處理,這種處理方式需要提前準備好 WebP 圖片。如何將圖片轉換為 WebP 格式

    • 使用 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 格式圖片,否則就響應請求圖片。

對比兩種處理方式,通過服務端來支援 WebP 具有如下優勢:

  • 提前知道客戶端是否支援 WebP。處理更靈活,更可靠。而客戶端還需要根據是否支援 WebP,對連結做額外的替換處理。
  • 動態支援 WebP。如果支援 WebP,檢視本地是否有對應 WebP 圖片,如果沒有動態生成響應。

服務端動態支援 WebP

服務端要動態支援 WebP,可以由代理伺服器 Nginx,或 Backend 來完成。

singsong:圖片處理邏輯最好交給下游 Backend 來完成,NGINX 就負責轉發即可。當然也有自動處理圖片 nginx :ngx_pagespeed

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。

相關文章