Eureka 主题性能优化小结

提醒:本文最后更新于 285 天前,文中所描述的信息可能已发生改变,请谨慎使用。

Featured Image

我在之前的文章 《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.jseureka.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,加载速度已经令我比较满意了。之后我再抽空处理下整站的图片资源。

Avatar
怡红公子 擅长前端和 Node.js 服务端方向。热爱开源时常在 Github 上活跃,也是博客爱好者,喜欢将所学内容总结成文章分享给他人。

8 评论

Charles Chin Chrome99.0 Windows 10
2022-03-22T14:25:39.935Z 回复

图标最后我选择了使用 iconfont,可以只选择自己需要的图标,阿里的 CDN 也还行。

怡红公子 Chrome99.0 Mac OS 10.15.7
2022-03-23T14:29:34.440Z 回复

@Charles Chin: 你这样可维护性更好一点,不过我这是想着和主题的搞一样的就没那么弄了,之后参考下这个方案。

Charles Chin Chrome99.0 Windows 10
2022-03-28T13:07:07.260Z 回复

@怡红公子: 字节的 Iconpark 支持 Web Component 和 SVG Symbol 两种方式引用。

Choicky ZHOU Edge98.0 Android 12
2022-03-02T03:12:49.666Z 回复

能分享一下优化后的主题不? 😀😀

怡红公子 Chrome98.0 Mac OS 10.15.7
2022-03-12T13:16:40.822Z 回复

@Choicky ZHOU: 优化后的主题可以结合我之前的那篇文章 https://imnerd.org/custom-hugo-theme-eureka.html 一块看下挑选需要的就可以了。如果有什么不懂的可以随时沟通~

liuguanyu Chrome98.0 Mac OS 10.15.7
2022-02-23T11:07:12.011Z 回复

手动👍公子

怡红公子 Chrome98.0 Mac OS 10.15.7
2022-02-23T11:17:04.474Z 回复

@liuguanyu: 二哥么么哒~ 二哥你的火麒麟还在诶,好久没更新啦,要加油哦~

熙龙 Chrome98.0 Windows 10
2022-02-21T03:27:45.563Z 回复

太强啦