ChrAlpha's Blog

Thumbnail-WebP%20%E5%9B%BE%E7%89%87%E4%BC%98%E5%8C%96%E5%9C%A8%20Hexo%20%E4%B8%8A%E7%9A%84%E6%9C%80%E4%BD%B3%E5%B0%9D%E8%AF%95

WebP 图片优化在 Hexo 上的最佳尝试

2020-05-30·笔记本

早已对 WebP.sh 垂涎三尺,但是出于一些个人情怀,并且为了将维护成本降至足够低,我选择了使用 Hexo 生成、部署在 GitHub Page 上的静态博客。虽然手头上确实也有些闲置 VPS,但是不想为此打乱阵型,自然也是享受不到 WebP.sh 真正的「无痛迁移」了。

更何况,即便浏览器对 WebP 的 支持情况 已经接近 80%,却依然有些主流浏览器如 Safari、IE 仍不支持,所以不能直接转用 WebP。

那难道,在 Hexo 等静态博客上就没有一套足够简单 WebP 解决方案吗?并不,我倒是尝试出一种勉强算是「无痛迁移」的实现。

优劣

本文步骤较多,所以先列举这套方案的优劣。仅供参考,方便你决定是否这么做。

优点

首先达成一个共识:所有网站都应该高效地压缩图像,并且由于压缩图像这项工作重复且繁琐,图像优化应自动化完成

  • 初次配置完成,日后使用无需任何操作便可全自动切换 WebP 图片格式
  • 对于不支持的浏览器,会自动回退到 JPEG/PNG 等传统格式
  • 提前生成好两份文件而非请求时计算,节省算力且响应更迅速

无论是出于节省带宽亦或是优化资源配置,你都应该尝试优化图像资源。而更小的页面资源链意味着更优的网页性能,这又能为你的网站获取更多的 SEO 分数。

劣势

人无完人,金无足赤。这种纯静态方案自然有着难以弥补的痛点。

  • 图片必须放在博客 source 文件夹内,不能是第三方图床(除非你愿意为每张图片手动配置)
  • 基于 gulp,每次构建部署时间可能会显出增长(其实你完全可以丢给 CI 完成,这算半个缺点吧)

在你权衡完优劣打算继续看后续详细步骤前,我想先说明:像是阿里云、腾讯云、又拍云等诸多云服务已提供自适应 WebP 功能,只需开关即可。如果你能做好一定的防护措施,使用他们的服务似乎也是一种好选择。当然,由于某些原因使用不了国内云服务的我只好另辟蹊径,才促成此文。

WebP 准备

WebP 是 Google 最新开发的新图像格式,旨在以可接受的视觉质量为无损和有损压缩提供较小的文件大小。有损模式下比 JPEG 小 25% - 34%,无损模式下较 PNG 小 26%。

如果你想详细了解这其中的技术细节,可以阅读 Google 开发者文章 WebP 压缩技术

根据 Hexo 规则,每次构建时会将 source/ 资源目录下的所有资源(包括图片资源)放入 public/ 网站目录中,随后将 public/ 网站目录部署到相应位置。所以,为了方便后续自动化实现,请将所有图片资源放于 source/ 资源目录下。本文中所有图像资源示例均放置于 source/assets/image/ 下以便演示。

Gulp,一款基于 node 实现的自动化构建工具。这里将借助 Gulp 以及 gulp-webp 全自动将图片转为 WebP 格式。

首先是安装:

npm install --global gulp-cli

npm install --save-dev gulp gulp-webp

并在博客根目录下创建 gulpfile.js 文件:

const gulp = require('gulp');
const webp = require('gulp-webp');

gulp.task('default', () => (
    gulp.src('./assets/image/*.{jpg,png,jpeg}')
        .pipe(webp({
            quality: 75,
            preset: 'photo',
            method: 6
        }))
        .pipe(gulp.dest('./assets/image'))
));

这样每次要批量转换位于 source/assets/image/ 下的图片格式时,只需要执行:

gulp

更新

上述方法将图片转为 WebP 格式还是有点不够优雅,它只能处理 source/assets/image/ 下图片,而对 source/assets/image/a/ 之类包含在子文件夹内图片就无动于衷了。

当然,这不过是再递归文件夹的事,但已经有前人写过命令行批量将图片转换 WebP 格式的 模块,我就不再造轮子了。

npm install --save-dev webp-batch-convert
npx cwebp-batch --in assets/image --out assets/image -q 75 -quiet

至此,所有准备工作都已经完成。在 source/assets/image/ 下的图片都同时拥有一份传统格式和一份 WebP 格式的文件,这两份文件仅后缀不同

启用与回退

前面提到,WebP 并没有得到完全支持,不能直接在网站中全部转换。对于不支持 WebP 的浏览器最终可能根本无法显示图像,我们当然不希望发生这种情况。

一开始我的想法是,通过引入 JavaScript 判断浏览器,如果不兼容则批量切换回原图。且不提我不希望引入过多 JS 这种怪癖,有些浏览器要视版本而定,有些小众浏览器压根没有记录,更何况 user-agent 还是可以伪造的。

后来注意到 <picture> 标记,该标记可以使用多个 <source> 元素和一个 <img> 标记。将 WebP 格式文件放入 <source> 元素中,并将应用更加广泛的 JPEG/PNG 等传统格式文件放入 <img> 标记中。对于能够理解 image/webp 源的浏览器会加载 <source> 标记内的 WebP 格式文件,而不理解的浏览器则回退至 <img> 标记内的传统格式文件。

<picture>
    <source srcset="/assets/image/beauty.webp" type="image/webp">
    <img src="/assets/image/beauty.jpg" alt="beauty">
</picture>

只要是能够理解 WebP 格式的浏览器都会加载 WebP 格式的图片,不理解的浏览器也能展示传统格式图片而不会完全不显示图片。这样一来,摆脱了对浏览器型号判断的硬性依赖,即便后续浏览器更新导致的支持情况变化,或者是一些没有姓名的小众浏览器,都能较好的处理,无需再更改配置。

在「Hexo 图片 lazyload」尝试了在 scripts/ 下放一个用于转换的脚本,每次 Generate 的时候就会自动转换。这种操作同样可以应用于此。

首先在博客根目录配置文件中追加:

use_webp: true

接下来在 scripts/ 下:

// scripts/webp/index.js

'use strict';

if (hexo.config.use_webp) {
    hexo.extend.filter.register('after_render:html', require('./lib/process').processWebP);
}
// scripts/webp/lib/process.js

'use strict';

const fs = require('hexo-fs');

function webpProcess(htmlContent) {
    return htmlContent.replace(/<img(.*?)src="(.*?)"(.*?)>/gi, function (str, p1, p2) {
        if (/webp-comp/gi.test(p1) || !/\/assets\/image\/(.*?)\.(jpg|jpeg|png)/gi.test(p2)) {
            return str;
        }
        return `<picture><source srcset="${p2.replace(/\.(jpg|jpeg|png)/gi, '.webp')}" type="image/webp">${str.replace('<img', '<img webp-comp')}</picture>`;
    });  
}

module.exports.processWebP = function (htmlContent) {
    return webpProcess.call(this, htmlContent);
}

至此,你的网站已经可以自动判断并显示 WebP 格式图片了。

兼容 Lazyload

关于如何实现 Lazyload 不是本文的重心,你可以参考我之前写的一篇文章「Hexo 图片 lazyload 的更优尝试」进行适配。

在图片进入可视区域前,我们自然也是希望 <source> 元素中同为 缩略图/占位元素 而非原图,不然懒加载就没有意义了。所以我们需要修改上述代码,请确保 webp 脚本在 lazyload 脚本之后执行。

为了方便,我将这两个 scripts 结合到一起。

// scripts/lazy-webp/index.js

'use strict';

if (hexo.config.lazyload && hexo.config.lazyload.enable === true) {
    if (hexo.config.lazyload.onlypost) {
        hexo.extend.filter.register('after_post_render', require('./lib/process').processPost);
    } else {
        hexo.extend.filter.register('after_render:html',  require('./lib/process').processSite);
    }
}

if (hexo.config.use_webp === true) {
    hexo.extend.filter.register('after_render:html',  require('./lib/process').processWebP);
}
// scripts/lazy-webp/lib/process.js

'use strict';

const fs = require('hexo-fs');

function lazyProcess(htmlContent)  {
    let loadingImage = this.config.lazyload.loadingImage || 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEXMzMyWlpYU2uzLAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg==';
    return htmlContent.replace(/<img(.*?)src="(.*?)"(.*?)>/gi, function (str, p1, p2) {
        // might be duplicate
        if (/data-srcset/gi.test(str)){
            return str;
        }
        if (/src="data:image(.*?)/gi.test(str)) {
            return str;
        }
        if (/no-lazy/gi.test(str)) {
            return str;
        }
        return str.replace(p2, p2 + '" class="lazyload" ' + 'data-srcset="' + p2 + '" srcset="' + loadingImage);
    });
}

function webpProcess(htmlContent) {
    return htmlContent.replace(/<img(.*?)data-srcset="(.*?)"(.*?)srcset="(.*?)"(.*?)>/gi, function (str, p1, p2, p3, p4) {
        if (/webp-comp/gi.test(p1) || !/\/assets\/image\/(.*?)\.(jpg|jpeg|png)/gi.test(p2)) {
            return str;
        }
        return `<picture><source class="lazyload" data-srcset="${p2.replace(/\.(jpg|jpeg|png)/gi, '.webp')}" srcset="${p4}" type="image/webp">${str.replace('<img', '<img webp-comp')}</picture>`;
    });
}

module.exports.processPost = function(data) {
    data.content = lazyProcess.call(this, data.content);
    return data;
};

module.exports.processSite = function (htmlContent) {
    return lazyProcess.call(this, htmlContent);
};

module.exports.processWebP = function (htmlContent) {
    return webpProcess.call(this, htmlContent);
}

CI 持续集成

我之前也写过关于 Hexo 持续集成部署文章「初探无后端静态博客自动化部署方案」,如有兴趣可配合该文阅读。

至于选择持续集成的理由,一方面可以节省本地图片格式转换的时间,另一方面 WebP 格式的图片甚至都不用保存在本地。

这里我选择了我正在使用的 GitHub Actions,其他几种也可类比。

name: Hexo Deploy Automatically

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout
      uses: actions/checkout@v2
      
    - name: Node.js envs
      uses: actions/setup-node@v1
      with:
        node-version: "10.x"
    
    - name: Hexo deploy
      env:
        HEXO_DEPLOY_KEY: ${{ secrets.HEXO_DEPLOY_KEY }}
      run: |
        mkdir -p ~/.ssh/
        echo "$HEXO_DEPLOY_KEY" > ~/.ssh/id_rsa
        chmod 600 ~/.ssh/id_rsa
        ssh-keyscan github.com >> ~/.ssh/known_hosts
        git config --global user.name "Your GitHub UserName"
        git config --global user.email "you@example.com"
        npm i -g hexo-cli gulp-cli
        npm i
        hexo cl && gulp
        hexo g -d

在迁移至 WebP 之前,我一直使用 MozJPEG 按质量分数 75/60 压缩。但是 JPEG 会出现难看的块效应伪影,也有读者向我抱怨说「博客的图片压缩到快要没法看了」。

后来在好朋友 木子 的博客看到 WebP.sh 的介绍,但是要维护服务器,只好放弃。

这次尝试,尽管不能像 WebP.sh 那样真正什么都不用管,但也算一劳永逸的方案。重点是,部署在任何地方的纯静态 Hexo 博客都能用上。

之前的原图都散落在各个位置,我也实在没有精力一个个找回来了。不过日后博客图片将采用 WebP 无损压缩,体验应该会上来的说。


参考链接:

https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/automating-image-optimization

https://developers.google.com/speed/webp

WebP 图片优化在 Hexo 上的最佳尝试
本文作者
ChrAlpha
发布日期
2020-05-30
更新日期
2020-06-24
转载或引用本文时请遵守 CC BY-NC-SA 4.0 许可协议,注明出处、不得用于商业用途!
CC BY-NC-SA 4.0