
![](https://imnerd.org/assets/migration/i.loli.net/2020/11/07/B6GZn1V2AS8XYIT.jpg)


我在之前的文章 [《Hugo 主题 Eureka 自定义》](https://imnerd.org/custom-hugo-theme-eureka.html) 中有讲到我现在用的博客主题就是 Eureka。不过主题虽然好看，但是性能跑分却比较低。遂趁着周末时间给优化了一下，遂有本文。

![](https://imnerd.org/assets/2022/eureka-theme-performance-optimize/1.png)

打开控制台看了下资源的加载，之前没注意，这会才发现首页竟然后 10M 这么多资源要加载，怪不得性能不好呢。

## JS 资源

JS 资源中大头是 FontAwesome，主题中直接使用了引用了所有图标的集成版地址 [@fortawesome/fontawesome-free/js/all.min.js](https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.4/js/all.min.js)，该资源有 1.2M。但其实在主题中根本没有使用到如此之多的图标，完全可以按需加载优化。

![](https://imnerd.org/assets/2022/eureka-theme-performance-optimize/2.png)

参考[《Using Font Awesome Icons in Hugo》](https://www.client9.com/using-font-awesome-icons-in-hugo/) 中的优化方法。通过关键词查找收集了主题中用到的图标，下载下来后通过模板语法直接在构建阶段把所有的 SVG 内联到 HTML 中。

不过我发现在首页中会有大量的重复图标，使用该方法后会有重复的 SVG 内容内联到 HTML 中。所以我再上述方法的基础之上再次尝试优化，将所有的 SVG 图标合并到一个文件中，每个使用的地方使用 `<use href="#<icon>" />` 来进行引用。

首先我们还是像之前那样，把所有的图标下载下来。区别是通过 `<symbol>` 将图标转成图元，方便后续使用 `<use>` 进行复用。

```js
// 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。

```html
<!--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 但是原理都差不太多。不过目前这种程度我也能接受了，就没有再继续尝试下去，之后有空再参考优化一下。

- [《本博客零散优化点汇总》](https://imququ.com/post/summary-of-my-blog-optimization.html#toc-2) 
- [《使用 Cloudflare Workers 加速 Google Analytics》](https://blog.skk.moe/post/cloudflare-workers-cfga/)
## CSS 资源

CSS 资源中大头是 `eureka.min.css`，高达 4M 的体积一看就知道它用了原子类 CSS 库 TailWind（笑哭。毕竟正经人谁能写出 4M 的 CSS 文件，特别还是这么简单的一款主题。

![](https://imnerd.org/assets/2022/eureka-theme-performance-optimize/3.png)

我对原子类 CSS 写法一直不太感冒的原因主要有两点，一个是本质它把 CSS 的功能转嫁到了 HTML class 属性上，看着那些纷繁复杂又臭又长的 class 令人脑壳疼。再一个就是因为它的体积问题。

好在 TailWind CSS 提供了优化选项，通过遍历配置中的文件查找所有可能用到的 class 节省体积。具体的话可以参考[文档](https://v2.tailwindcss.com/docs/optimizing-for-production#purge-css-options)。由于是静态分析 class，所以不能出现动态拼接，也不能出现变量之类的替代。

将配置开启之后，`eureka.min.css` 文件从初始的 4M 优化成了 21.4k，Brotil 压缩后体积是 5.2k，整个人都神清气爽了有没有。

初次之外，网站还加载了一款代码高亮主题 `solarized-light.min.css` 以及一款自定义字体。代码高亮样式则按照 JS 优化策略一样，仅针对详情页再加载。而自定义字体我看了下会加载一款中文的 Web Font，用于提供给全站使用。所以也没有做动态切片等体积优化处理，每次都会加载 2M 的字体资源。考虑到该需求是纯美化场景，系统默认的衬线体也还可以，遂直接将该自定义字体移除解决。

![](https://imnerd.org/assets/2022/eureka-theme-performance-optimize/5.png)
## 图片资源

图片也是比较中的资源加载灾区，有 3M 的图片资源加载。由于之前没有特别在意这块，很多场景为了方便直接原图就放上来了，也没有做图片的处理。所以这次就使用常规的图片资源处理手段对图片进行了优化处理，主要是图片压缩以及 LazyLoad。

![](https://imnerd.org/assets/2022/eureka-theme-performance-optimize/4.png)

图片处理这块就是很正常的手段了，没有什么值得说的。图片压缩主要是使用了 <https://tinypng.com>，LazyLoad 则是用了[苏卡卡推荐](https://blog.skk.moe/post/how-to-make-a-fast-blog/#%E5%BB%B6%E8%BF%9F%E9%9D%9E%E5%85%B3%E9%94%AE%E8%B5%84%E6%BA%90%E5%8A%A0%E8%BD%BD)的 [vanilla-lazyload](https://github.com/verlok/vanilla-lazyload)。

除了体积的优化之外，图片还可以对它进行格式和尺寸进行优化。现在比较推荐使用 `<picture>` 的写法渲染图片，内部存放不同的格式的图片，浏览器会根据是否支持选择对应的格式展示。

```html
<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 本身也有非常多的图片处理方法用于图片优化，主要是[裁剪](https://gohugo.io/content-management/image-processing/)和[滤镜](https://gohugo.io/content-management/image-processing/)。我们可以利用 Hugo 本身的功能，也可以使用外部 CDN 存储服务，基于他们的动态裁剪能力来实现。

不过我的只是我的文章中图片地址五花八门，有本地的也有各种外部 CDN 的，使用那种方式都比较麻烦。~~所以暂时就没有处理格式的事情了，~~ 如果有需要的可以参考一下。

> **2022-07-02 更新**
> 
> 最终我采用了外部 CDN 的方式，将博客中所有的外链图片重新抓取下来整理上传到又拍云，并对文章图片进行了整体的清洗，一些老图无法访问的就直接指向一个 404 的图片了。
>
> 同时开启了又拍云的 Webp 自适应功能，无需修改图片链接地址，又拍云 CDN 会自动根据浏览器是否支持来返回 Webp 图片，轻松全站支持 Webp 图片访问。最终的优化效果也非常明显，整体图片体积再次缩小了 3 倍左右。
> 
> ![](https://imnerd.org/assets/2022/eureka-theme-performance-optimize/6.jpg) 

## 总结

在各种优化之下，最终首页的资源加载从之前的 10M 缩减到了现在的 361k，加载速度已经令我比较满意了。之后我再抽空处理下整站的图片资源。