这个网站是静态网站,是用我自己编写的静态网站生成器搭建的。我在本地开发网站时,希望它能快速加载。网站规模相对较小,现在的电脑性能都很强大,所以我不想浪费时间等待。渲染所有 HTML 页面大约需要 15 秒——速度慢到每次都能感觉到延迟。
我使用 Jekyll 时,启用 Jekyll 缓存后速度提升显著。很多耗时的计算其实不需要每次建站都重复执行——例如,将一段 Markdown 代码转换为 HTML 只需执行一次,即可永久缓存。
由于我不再使用 Jekyll,所以我用一个基本的 SQLite 缓存替换了 Jekyll 缓存。我选择 SQLite 是因为它速度快、易于使用,而且可以与Python 标准库一起使用。
每个缓存条目都包含三个部分:命名空间、键和值。命名空间将来自同一操作的所有条目分组,键标识单个条目,值是耗时计算的输出结果。例如,在我的 Markdown 转 HTML 缓存中,命名空间是convert_markdown ,键是输入的 Markdown 文件,值是输出的 HTML 文件。
目前我只存储基本的字符串值。我可以将结构化数据存储为 JSON 或其他格式,但我目前还没有这个需求。
我的缓存实现是用 Python 编写的,但它只是对 SQLite 查询的一个简单封装。
SQLite 查询
创建一个空缓存:
CREATE TABLE IF NOT EXISTS cache_entries ( namespace , key , value , date_saved , PRIMARY KEY ( namespace , key ) )
这将创建一个名为cache_entries的空表,包含四列:之前提到的命名空间/键/值对,以及用于调试的date_saved列。我原本以为记录缓存条目的保存时间会很有用,但目前为止还没用到。
复合PRIMARY KEY确保对于给定的命名空间/键对,我只有一个缓存条目。
为了存储缓存条目,我使用标准的 SQL INSERT OR REPLACE语句:
INSERT OR REPLACE INTO cache_entries VALUES ( ? , ? , ? , ? );
要检索缓存条目,我使用标准的SELECT :
SELECT value FROM cache_entries WHERE namespace =? AND key =? ;
我发现,如果缓存值很大,这个查询会明显变慢,因为 SQLite 需要读取很多页才能检索到该值。有时我只想知道某个值是否被缓存,并不需要知道它的具体内容——缓存条目的存在本身就能让我跳过一些操作。
我还有另一个查询来检测缓存中是否存在匹配的条目,这个查询速度快得多,因为它跳过了读取值这一步:
SELECT EXISTS ( SELECT 1 FROM cache_entries WHERE namespace =? AND key =? )
最后,我还有几个查询语句用于清除缓存——既可以清除单个条目,也可以清除整个操作:
DELETE FROM cache_entries WHERE namespace =? AND key LIKE ? ; DELETE FROM cache_entries WHERE namespace =? ;
选择缓存键
对于较小的输入,我将输入用作缓存键。
对于大型输入(例如整篇博客文章的 Markdown 内容),我使用 MD5 哈希值作为键,而不是原始输入。这可以减少写入磁盘的数据量,从而加快数据库速度。SQLite 使用 4KB 的页面,这比我的许多博客文章都要小。一个 4KB 的页面可以存储大量的 MD5 哈希值,而一篇原始博客文章则需要占用多个页面。这部分逻辑是在缓存代码之外处理的。
当结果依赖于外部文件(例如渲染模板)时,我会将该外部文件的最后修改时间包含在缓存键中。当外部文件发生更改时,会发生缓存未命中,需要重新计算结果。
进展如何
缓存机制需要一些微调。缓存失效向来是个难题,而且我确实经常没能正确地使缓存失效。在构建网站的正式版本时,我会删除现有的缓存并重新开始,以避免缓存条目过期。
对于本地开发而言,这无疑是一项巨大的进步。以前重新渲染所有 HTML 页面大约需要 15 秒,但有了热缓存后,只需 0.06 秒。速度提升了 200 倍,每次点击保存按钮我都能明显感受到,这让网站开发体验更加流畅、更加令人满意。
[如果您的RSS阅读器中此文章的格式显示异常,请访问原文]