2026.06.08 (一)

✨ GPT-5.5 的摘要  

多语言扩展后超过 20 分钟的 Jekyll 构建,我用 profile 追踪原因,移除重复渲染和全站扫描,最终降到 1 分 50 秒的记录。

前一篇多语言导入文章中,我给博客加上了多语言运营结构。

一开始这件事还挺让人得意。

ko, en, ja, zh-Hans, es, pt-BR, fr, id

有文章,有菜单,有 hreflang,浏览量也能共享。从外面看,已经像一个还不错的多语言博客了。

但问题马上从另一个方向冒了出来。

构建太慢了。

第一次 production build 是这样的。

done in 1311.791 seconds

21 分 52 秒。

这已经不像博客了,更像是按下部署按钮之后开始祈祷的东西。只改一篇文章,构建就要等 20 多分钟,那之后写文章的时间可能还没有等构建的时间长。

一开始也可以很简单地想:“文章变成 8 种语言了,所以当然变慢吧。”

但这个结论太草率了。

页面数增加是事实。可慢到 20 分钟级,里面一定还有别的东西。所以这一次我没有靠感觉乱改,而是打开了 jekyll build --profile

第一个凶手:把日历 JSON 塞进了每个页面

我先看了 _site 的大小。

_site: 1.2G
HTML total: 约 840MB

静态博客的 HTML 有 840MB。

这说不过去。

打开一篇代表性文章,原因立刻露出来了。侧边栏日历在每一个文章页面里,都把对应语言的完整文章列表 JSON inline 进去了。

<script type="application/json" data-calendar-posts>
[
  ...
]
</script>

还不止如此。

生成日历 fallback 列表时,每个日期又重新扫一遍完整文章列表。即使不符合条件的文章,也会通过 Liquid 空白大量输出。屏幕上看到的东西很小,但 HTML 里面藏着巨大的空白和重复列表。

这不是功能问题,而是结构问题。

日历不需要服务器在每个页面上都生成完整 HTML。既然 JS 本来就能动态绘制日历,服务器只要给出当前月份的基本骨架和数据文件路径就够了。

所以我把各语言的日历数据拆成了独立 JSON 文件。

/assets/data/calendar-posts-ko.json
/assets/data/calendar-posts-en.json
/assets/data/calendar-posts-ja.json
/assets/data/calendar-posts-zh-Hans.json
/assets/data/calendar-posts-es.json
/assets/data/calendar-posts-pt-BR.json
/assets/data/calendar-posts-fr.json
/assets/data/calendar-posts-id.json

页面里只留下这种程度的信息。

data-calendar-posts-src="/assets/data/calendar-posts-en.json"

结果立刻降了下来。

1311.791秒 -> 745.273秒

几乎砍掉了一半,但还是很长。

这时我明白了。

日历是大凶手,但不是唯一的凶手。

第二个凶手:菜单统计被计算了 8 万次

下一个 profile 更直白。

_includes/sidebar-nav-stats.html  81120 calls  173.084s
_includes/masthead.html            2704 calls  319.860s
_includes/seo.html                 2704 calls  119.985s
sitemap.xml                           1 call   117.495s

这里最荒唐的是 sidebar-nav-stats.html

这个 include 只是侧边栏分类旁边显示文章数量和最新文章时间的一小块。

比如这样。

Daily Review (310) 1 days ago
Devlog (24) 2 days ago

可是这个小片段每次被调用时,都在重新排序完整文章列表,再重新过滤一遍。

每个语言的每个菜单项都做一次。

每个页面都做一次。

桌面侧边栏和移动端菜单又各做一次。

结果就是被调用了 81,120 次。

这个值其实完全没有必要每页重新算。相同语言、相同菜单 URL,结果就是一样的。所以我把它改成了 jekyll-include-cacheinclude_cached

{% include_cached sidebar-nav-stats.html url=child.url lang=current_lang %}

于是调用数变成了这样。

81120 calls -> 210 calls
173秒 -> 0.4秒

这几乎已经是抓到 bug 的程度了。

第三个凶手:为了生成语言链接一直扫全站

加上多语言切换时,masthead, seo, sitemap 里进了这样的逻辑。

这个语言 URL 真的存在吗?

意图是对的。

不存在的翻译 URL 不能放进 hreflang 或语言切换菜单里。所以一开始我遍历了所有页面和所有 collection 文档,来确认 URL 是否存在。

问题在于,这件事在每个页面上都重复了一遍。

2704 pages * site.pages scan * translated collections scan

这是多语言站点越大就越恶化的结构。

所以我换了方式。

这个博客的翻译 URL 本来就有规则。

/some/post/
/en/some/post/
/ja/some/post/
...

例外则单独用数据管理就可以。

这次仍在暂缓翻译的会话回顾文章,被放进了 _data/i18n_pending.yml

entries:
  - source_url: /devlog/github-pages-blog/github-pages-blog-english-version-lessons/
    locales:
      - en
      - ja
      - zh-Hans
      - es
      - pt-BR
      - fr
      - id

这样普通文章就按 prefix 规则连接,暂缓文章则 fallback 到对应语言首页。不再扫全站。

结果很大。

masthead: 319.860秒 -> 9.882秒
seo:      119.985秒 -> 7.181秒
sitemap:  117.495秒 -> 4.633秒

最终构建这样结束。

done in 110.344 seconds

从 21 分 52 秒,到 1 分 50 秒。

这还不能说是很快的博客,但至少已经脱离了“因为构建太可怕所以不敢写文章”的状态。

修的时候小心的事

这次优化只看速度,好像很简单。

但真正要小心的是功能被弄坏。

尤其是多语言站点,很容易因为构建快了而高兴,结果出现这种问题。

语言切换链接跳到 404
hreflang 指向不存在的 URL
暂缓翻译的文章作为 alternate 暴露给搜索引擎
移动端 About/语言按钮又看不见
日历保持空状态

所以最后我同时用了浏览器和自动检查。

确认的内容是这些。

production build 成功
英语旅行文章的 8 种语言切换链接正常
hreflang 8 种语言 + x-default 正常
暂缓翻译文章 fallback 到其他语言首页
移动端 About, 🇺🇸English, 日历显示正常
代表性多语言 URL 200
源文章 2481 个渲染结构全量确认
日历 JSON 解析确认
i18n post coverage errors: 0

当然不是人眼把 2481 篇文章一篇篇读完。

但至少“输出文件是否存在”“文章结构是否渲染”“语言链接是否断裂”“日历数据是否存在”,这些都通过自动检查全量确认了。

这才是这次工作的核心。

构建优化不只是减少时间,而是重新明确既有功能的契约。

这次学到的事

Jekyll 是静态站点生成器,所以看起来很简单。

但一旦在 Liquid 里不断扫描整个站点,静态站点也完全可以变得很重。

尤其是多语言结构里,小小的低效率会立刻被倍数放大。

页面数 * 语言数 * 菜单数 * 全部文章数

这种乘法藏在里面,之后构建会突然爆掉。

这次学到的事情很简单。

第一,不要在每个页面里 inline 重复同一份数据。

第二,相同输入的 include 要缓存。

第三,不要为了确认 URL 是否存在,就在每个页面上扫描全站。

第四,例外不要凭感觉处理,要放进数据。

第五,优化之后,要用自动检查确认功能契约。

这个博客现在正从单纯的个人博客,一点点变成系统。

这既好,也累。

但至少这一次,累的那一面是有意义的。

把一个要等 21 分钟以上的构建拉到 1 分钟级,体感上是很大的转折点。

现在继续把多语言博客养大,至少有了最基本的一口气。

留下评论