Katex Server Side Rendering With Hugo
将$\KaTeX$引入到Hugo里边几乎不费什么力气,并且能得到很好的显示效果和性能。但是如果文件比较大的时候就很糟心了,每次重新加载都要重新渲染,在一般配置的电脑上加载这篇公式较多的文章的时候要耗时10s以上。
分析了一下加载的流程,Katex的渲染以及js脚本的下载都占用了不少的时间。这是因为使用的是Client Side的渲染方法,要先下载js再处理数学公式的内容,每次加载都要如此,那能不能在Server Side直接渲染呢,尤其是在Hugo这静态网站上面Once for All?
这篇文章1比较详细的综述了到目前为止(2022年)Katex在Server Side的渲染情况,主要有几种解决方法:
- 让Hugo使用一个goldmark扩展,在这个扩展里边渲染Katex,目前仅有的是
goldmark-qjs-katex
,但是这个扩展因为有不可移植代码的原因压根就没有被上游接受,不得已其作者自己fork了一份Hugo,但是似乎又没有精力维护跟上游版本的同步2,导致落后Hugo不少版本。 - Hugo是支持pandoc的,而pandoc是支持Katex渲染的,也就意味着文档的格式要用pandoc来写,这篇文章3记录了这个过程。
- 或者尝试直接将扩展写到Hugo里边,但是并没有真正的在做Server Side渲染
上面的方法都是努力将渲染的过程集成到Hugo当中去,实现无缝的工作流,但是这个目标目前看来还很难实现。 作者花了很多的时间去调研,最后不得已使用了Client Side的渲染方式,看起来也是相当的无奈。
如果改变不了Hugo又不想切换其他工具的话,那就只能改变自己了。
事实上,考虑到直接调用katex.renderToString
就可以生成html,我们拿到Hugo输出html文件的后可以在自己的workflow上面加一个后处理的stage来完成替换就可以了。这是因为数学公式是以$
分割的,如果我们能定位到这些内容,用一些替换规则就可以完成简单的渲染了。
这里选择使用正则表达式匹配到$
中间的内容,然后调用Katex的函数生成html再替换原来的内容,注意这部分的配置要跟Client Side渲染的参数完全一样才能达到一致的效果。
const delimiters = [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false },
];
for (var delim of delimiters) {
const regex = new RegExp(
escapeRegex(delim.left) + "([\\S\\s]*?)" + escapeRegex(delim.right),
"g"
);
html = html.replace(regex, function (match, g1) {
return katex.renderToString(decode(g1), {
displayMode: delim.display,
output: "html",
strict: false,
macros: {},
});
});
}
可以想象得到的是这种方法会有误判,比如代码里边如果有$
的话就会出现渲染错误。实际运行的时候发现错误分为两种,一种是直接渲染成功了但是渲染的结果有问题,这种对我来说是可以接受的,因为在我常用的语言中似乎没有对$
有什么特殊的青睐,出现的概率很低,可以用替换的方式规避;
var self = new Error(error);
^
ParseError: KaTeX parse error: Expected 'EOF', got '#' at position 30: …n style="color:#̲a6e22e">right</…
另一种是直接程序异常了,比如上面在渲染这篇文章的时候code block里边$
,并且中间有一些颜色变量,就导致了Katex渲染失败,这种就要么disable掉Katex的throw error的功能,接受可能出现的排版错乱,要么像我对这篇文章的处理一样加入白名单不进行再渲染,或者用其他符号代替然后进行说明,总体来看失败的概率还是很低的,属于可以接受的范围。
有了这个渲染脚本,再加上简单的读写文件操作就可以完成渲染的工作了,唯一缺的就是一个nodejs的运行环境了,这个时候就需要在CI里边增加setup环境的流程。我使用的是Github Action,创建一个nodejs的环境非常的简单
- name: Setup nodejs enviroment
uses: actions/setup-node@v3
with:
node-version: 18
有了nodejs环境,再增加一个调用脚本渲染的step就可以了,当然这个过程要放到Hugo产生静态网页之后。
- name: Render Katex on Server Side
run: |
node math.js <posts>
当然,这个stpes可以选择再封装一层用来处理所有的posts,这里就不赘述了。
另外,我们实际上不再需要下载Katex的三个js文件了,可以将其从html里边删除掉,额外提升一点性能,参考代码如下:
html = html.replace(/<script defer[\s\S]*?katex\@0\.16\.2[\s\S]*?>[\s\S]*?<\/script>/gm, "");
这就是我当前使用的渲染方式,使用这种方式前面的那篇长文章的加载速度从10s变到了5s左右,提升了50%的性能,效果还是不错的,而且对写作-发布的工作流也没有特别大的影响。
TODO:
未来如果正则匹配错误率太高的话可以选择parse html的方式,过滤掉code block再匹配替换。