9

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

 2 years ago
source link: https://blog.ichr.me/post/webp-on-hexo/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

早已对 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 "[email protected]"
        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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK