我在之前的文章 《Hugo 主题 Eureka 自定义》 中有讲到我现在用的博客主题就是 Eureka。不过主题虽然好看,但是性能跑分却比较低。遂趁着周末时间给优化了一下,遂有本文。
打开控制台看了下资源的加载,之前没注意,这会才发现首页竟然后 10M 这么多资源要加载,怪不得性能不好呢。
JS 资源
JS 资源中大头是 FontAwesome,主题中直接使用了引用了所有图标的集成版地址 @fortawesome/fontawesome-free/js/all.min.js,该资源有 1.2M。但其实在主题中根本没有使用到如此之多的图标,完全可以按需加载优化。
参考《Using Font Awesome Icons in Hugo》 中的优化方法。通过关键词查找收集了主题中用到的图标,下载下来后通过模板语法直接在构建阶段把所有的 SVG 内联到 HTML 中。
不过我发现在首页中会有大量的重复图标,使用该方法后会有重复的 SVG 内容内联到 HTML 中。所以我再上述方法的基础之上再次尝试优化,将所有的 SVG 图标合并到一个文件中,每个使用的地方使用 <use href="#<icon>" />
来进行引用。
首先我们还是像之前那样,把所有的图标下载下来。区别是通过 <symbol>
将图标转成图元,方便后续使用 <use>
进行复用。
// deno run --allow-net --allow-write fontsvg.ts
import * as path from "https://deno.land/std/path/mod.ts";
const __dirname = new URL('.', import.meta.url).pathname;
const icons = [
"calendar-alt",
"calendar",
"star-half-alt",
"comment",
"clock",
"bars",
"search",
"moon",
"sun",
"adjust",
"globe",
"th-list",
"folder",
"caret-right",
"edit",
"user",
"pen",
"book",
"rss"
];
const baseUrl = 'https://cdn.jsdelivr.net/gh/FortAwesome/Font-Awesome@5.x/svgs/solid';
const toDefs = (id: string, svgText: string) => svgText
.replace(/<svg.+viewBox=['"](\d+) (\d+) (\d+) (\d+)[^>]+>/, `<symbol id="${id}" viewBox="$1 $2 $3 $4">`)
.replace('</svg>', '</symbol>')
.replace('<path', '<path fill="currentColor"')
.replace(/<!--.+?-->/, '');
const iconTexts = await Promise.all(icons.map(async icon => {
const text = await fetch(`${baseUrl}/${icon}.svg`).then(resp => resp.text());
const match = text.match(/(viewBox="\d+ \d+ \d+ \d+")/);
if(!match) {
throw Error('match error');
}
Deno.writeTextFile(path.join(__dirname, `./fontawesome/${icon}.svg`), `<svg ${match[1]}><use href="#${icon}" /></svg>`);
return toDefs(icon, text);
}));
Deno.writeTextFile(path.join(__dirname, './fontawesome/all.svg'), `<svg width=0 height=0 viewBox="0 0 0 0">${iconTexts.join('\r\n')}</svg>`);
之后我们需要在 header 中加载 all.svg
。在主题 header.html
开头增加如下代码:
{{ $svg := resources.Get (print "fontawesome/all.svg") }}
{{ $svg.Content | safeHTML }}
还是像引文中的方式一样,我们定义一个 Partial,所有使用的地方可以直接使用这个 Partial 内联图标 SVG。
<!--layouts/partials/fontawesome.html-->
<span class="inline-svg svg-inline--fa fa-w-14 {{.class}}">
{{ $svg := resources.Get (print "fontawesome/" .icon ".svg") }}
{{ $svg.Content | safeHTML }}
</span>
最后我们在使用的地方只需要使用如下 partial 命令即可完成图标的嵌入。修改 calendar
为对应的图标名称可以实现内嵌对应的图标。
{{ partial "fontawesome.html" (dict "icon" "calendar") }}
这么优化之后首页 HTML 文档的体积有着显著的改善,从 89.7k 降低至 77.3k。不过由于内联的图标都变成了不重复的内容,压缩率反而降低了,这倒是我没有想到的。Vercel 使用的是 Brotil 压缩方式,原本基于引文的方式压缩后的体积是 20.4k,优化后压缩后的体积反而增加到了 22.1k。不过 2k 不到的体积增长,倒是还能接受。
解决了大头之后,JS 资源还剩下 highlight.min.js
和 eureka.min.js
,前者是代码高亮使用,后者是主题对应的 JS 脚本。由于我在首页实际上是没有代码高亮的需求,所以我将 highlight.js 相关的资源做了判断,仅在详情页的时候再做加载。
而针对 eureka.min.js
这种小文件,我们可以考虑将其内联到 HTML 中减少一个请求。不过该优化在 HTTP/2 场景并不是一个最佳实践,诸君请适度使用。
{{- $eurekaJS := resources.Get "js/eureka.js" | resources.ExecuteAsTemplate "js/eureka.js" . | minify }}
<script defer src="data:application/javascript;base64,{{ $eurekaJS.Content | base64Encode }}"></script>
最后其实还剩下百度统计的请求资源,这个参考以下两篇文章也是可以做类似的优化的,虽然两篇文章讲的都是 Google Analytics 但是原理都差不太多。不过目前这种程度我也能接受了,就没有再继续尝试下去,之后有空再参考优化一下。
CSS 资源
CSS 资源中大头是 eureka.min.css
,高达 4M 的体积一看就知道它用了原子类 CSS 库 TailWind(笑哭。毕竟正经人谁能写出 4M 的 CSS 文件,特别还是这么简单的一款主题。
我对原子类 CSS 写法一直不太感冒的原因主要有两点,一个是本质它把 CSS 的功能转嫁到了 HTML class 属性上,看着那些纷繁复杂又臭又长的 class 令人脑壳疼。再一个就是因为它的体积问题。
好在 TailWind CSS 提供了优化选项,通过遍历配置中的文件查找所有可能用到的 class 节省体积。具体的话可以参考文档。由于是静态分析 class,所以不能出现动态拼接,也不能出现变量之类的替代。
将配置开启之后,eureka.min.css
文件从初始的 4M 优化成了 21.4k,Brotil 压缩后体积是 5.2k,整个人都神清气爽了有没有。
初次之外,网站还加载了一款代码高亮主题 solarized-light.min.css
以及一款自定义字体。代码高亮样式则按照 JS 优化策略一样,仅针对详情页再加载。而自定义字体我看了下会加载一款中文的 Web Font,用于提供给全站使用。所以也没有做动态切片等体积优化处理,每次都会加载 2M 的字体资源。考虑到该需求是纯美化场景,系统默认的衬线体也还可以,遂直接将该自定义字体移除解决。
图片资源
图片也是比较中的资源加载灾区,有 3M 的图片资源加载。由于之前没有特别在意这块,很多场景为了方便直接原图就放上来了,也没有做图片的处理。所以这次就使用常规的图片资源处理手段对图片进行了优化处理,主要是图片压缩以及 LazyLoad。
图片处理这块就是很正常的手段了,没有什么值得说的。图片压缩主要是使用了 https://tinypng.com,LazyLoad 则是用了苏卡卡推荐的 vanilla-lazyload。
除了体积的优化之外,图片还可以对它进行格式和尺寸进行优化。现在比较推荐使用 <picture>
的写法渲染图片,内部存放不同的格式的图片,浏览器会根据是否支持选择对应的格式展示。
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="my image">
</picture>
剩下的就是如何获取 webp 的图片了,网上有比较多使用 cwebp
手动转成 webp 图片的教程,我就不多赘述了。除此之外,Hugo 本身似乎也支持做个格式的转换(https://discourse.gohugo.io/t/image-conversion-without-resizing/32429)。
{{ $i := resources.Get "image.jpg" }}
{{ $resizeOptions := printf "%dx%d webp" $i.Width $i.Height }}
{{ $i = $i.Resize $resizeOptions }}
最后一种方式,也是我比较推荐的方式,是使用外部的 CDN 存储服务。这些外部服务都会有通过 URL 动态转换和裁剪的能力。
除了更好的格式,对图片的裁剪也很重要。实际上 Hugo 本身也有非常多的图片处理方法用于图片优化,主要是裁剪和滤镜。我们可以利用 Hugo 本身的功能,也可以使用外部 CDN 存储服务,基于他们的动态裁剪能力来实现。
不过我的只是我的文章中图片地址五花八门,有本地的也有各种外部 CDN 的,使用那种方式都比较麻烦。所以暂时就没有处理格式的事情了, 如果有需要的可以参考一下。
2022-07-02 更新
最终我采用了外部 CDN 的方式,将博客中所有的外链图片重新抓取下来整理上传到又拍云,并对文章图片进行了整体的清洗,一些老图无法访问的就直接指向一个 404 的图片了。
同时开启了又拍云的 Webp 自适应功能,无需修改图片链接地址,又拍云 CDN 会自动根据浏览器是否支持来返回 Webp 图片,轻松全站支持 Webp 图片访问。最终的优化效果也非常明显,整体图片体积再次缩小了 3 倍左右。
总结
在各种优化之下,最终首页的资源加载从之前的 10M 缩减到了现在的 361k,加载速度已经令我比较满意了。之后我再抽空处理下整站的图片资源。
图标最后我选择了使用 iconfont,可以只选择自己需要的图标,阿里的 CDN 也还行。
@Charles Chin: 你这样可维护性更好一点,不过我这是想着和主题的搞一样的就没那么弄了,之后参考下这个方案。
@怡红公子: 字节的 Iconpark 支持 Web Component 和 SVG Symbol 两种方式引用。
能分享一下优化后的主题不? 😀😀
@Choicky ZHOU: 优化后的主题可以结合我之前的那篇文章 https://imnerd.org/custom-hugo-theme-eureka.html 一块看下挑选需要的就可以了。如果有什么不懂的可以随时沟通~
手动👍公子
@liuguanyu: 二哥么么哒~ 二哥你的火麒麟还在诶,好久没更新啦,要加油哦~
太强啦