速度就是关键! —— 我们是如何让 Hexo 4.2 的生成速度提升 30% 的
本文已翻译为英文并收录于 Hexo 官网:Speed is the Key - How We Make Hexo 30% Faster
对于 Hexo 来说,速度一直都是关键。三年前,通过模板预编译,Hexo 3.2 的生成性能相比 Hexo 3.1 提升了一倍。到了 Hexo 4.2,通过一系列改进,我们成功使 Hexo 4.2 的生成速度相比 Hexo 3.2 再提升了 30%。
Benchmark
Benchmark 的设置和环境如下:
- Travis CI - Ubuntu Xenial 16.04
- CPU:2 Cores
- RAM:7.5 GB
- Hexo 默认的 landscape 主题
- 随机产生的 300 篇文章:每篇文章都包含了所有的 Markdown 语法(标题、链接、图片),用于测试
highlight.js
的代码块;Front Matter 中设置了不重复的一个分类和三个标签。
由于 Hexo 3.2 开始将渲染结果存在 db.json
中,因此在 Benchmark 中同时测试了冷生成(heox g
之前先 hexo clean
删除 db.json
)和热生成(第二次 hexo g
之前不执行 hexo clean
)的性能数据。
每次 Benchmark 以 Cold => Hot
的顺序执行;内存占用使用 time
测量,取 Resident Set Size (RSS)
的值。
Benchmark 使用的脚本可以在 这里 查看。
Node.js 8
Hexo 3.2 | Hexo 3.8 | Hexo 4.2 | ||||
---|---|---|---|---|---|---|
Cold processing | 13.585s | 0% | 18.572s | +37% | 9.210s | -32% |
Cold generation | 13.027s | 0% | 50.528s | +284% | 8.666s | -33% |
Memory Usage (Cold) | 815.754MB | 0% | 1416.309MB | +69% | 605.312MB | -26% |
Hot processing | 0.668s | 0% | 0.712s | +6% | 0.732s | +7% |
Hot generation | 11.734s | 0% | 46.339s | +295% | 7.821s | -33% |
Memory Usage (Hot) | 702.535MB | 0% | 1450.719MB | +106% | 821.512MB | +17% |
Node.js 10
Hexo 3.2 | Hexo 3.8 | Hexo 4.2 | ||||
---|---|---|---|---|---|---|
Cold processing | 11.875s | 0% | 15.985s | +35% | 8.043s | -29% |
Cold generation | 10.308s | 0% | 41.339s | +301% | 7.450s | -28% |
Memory Usage (Cold) | 805.633MB | 0% | 1440.297MB | +79% | 599.008MB | -26% |
Hot processing | 0.700s | 0% | 0.676s | -3% | 0.731s | +4% |
Hot generation | 8.322s | 0% | 35.453s | +326% | 6.420s | -23% |
Memory Usage (Hot) | 679.082MB | 0% | 1447.109MB | +113% | 789.527MB | +16% |
Node.js 12
Hexo 3.2 | Hexo 3.8 | Hexo 4.2 | ||||
---|---|---|---|---|---|---|
Cold processing | 11.454s | 0% | 15.626s | +36% | 8.381s | -27% |
Cold generation | 10.428s | 0% | 37.482s | +260% | 7.283s | -30% |
Memory Usage (Cold) | 1101.586MB | 0% | 1413.359MB | +28% | 580.953MB | -47% |
Hot processing | 0.724s | 0% | 0.790s | +9% | 0.790s | +9% |
Hot generation | 8.994s | 0% | 35.116s | +293% | 6.385s | -29% |
Memory Usage (Hot) | 696.500MB | 0% | 1538.719MB | +120% | 600.398MB | -14% |
Node.js 13
Hexo 3.2 | Hexo 3.8 | Hexo 4.2 | ||||
---|---|---|---|---|---|---|
Cold processing | 11.496s | 0% | 14.970s | +29% | 8.489s | -26% |
Cold generation | 10.088s | 0% | 36.867s | +265% | 7.212s | -28% |
Memory Usage (Cold) | 1104.465MB | 0% | 1418.273MB | +28% | 596.233MB | -46% |
Hot processing | 0.724s | 0% | 0.776s | +7% | 0.756s | +4% |
Hot generation | 7.995s | 0% | 33.968s | +325% | 6.294s | -21% |
Memory Usage (Hot) | 761.195MB | 0% | 1516.078MB | +99% | 812.234MB | +7% |
从 Hexo 中去除 cheerio 依赖
正如 Benchmark 数据所示,Hexo 3.8 出现了严重的性能下降。我们发现 #3129 引入的 meta_generator
特性是罪魁祸首。#3129 使用 cheerio
往 HTML 的 <head>
中插入 <meta name="generator" content="Hexo [version]">
,使得 cheerio
需要将 Hexo 生成的所有 HTML 全部存进内存并解析成 DOM。
cheerio
很快,但是在遍历上百个 HTML 文件时时还是会遇到性能瓶颈。我们在 #3677 中提出提案去除 cheerio
依赖。先后经过 #3671、#3680、#3685,Hexo 在 open_graph()
helper、meta_generator
filter 和 external_link
filter 中用正则表达式替代了 cheerio
,并在 hexo-util#137 和 #3850 中将 toc()
所依赖的 cheerio
换成了更快的 htmlparser2
。到了 Hexo 4.2,我们从 Hexo 中彻底去掉了 cheerio
。
改善 Cache of Rendered HTML
机制
Cache of Rendered HTML
最早于 Hexo 3.0.0-rc4 中 e8e45ed
引入,试图通过缓存渲染结果来改善 Hexo 的生成性能。但是在 hexo g
中每一条路径只被使用一次,所以缓存并没有起到效果、白白占用了内存。在 #3756 中 Cache of Rendered HTML
被调整为只在 hexo s
下启用,大幅减少了 hexo g
的内存占用。
从 Hexo 中去除 Lodash 依赖
Lodash 是个实用的工具库,大大降低了 Array、Number、Objects、String 的使用难度。不过随着 ES6 的新特性不断增加,Lodash 的大部分功能都有了 Native 替代。
Hexo 其实很早就开始减少对 Lodash 的依赖了,如 #3285、#3290 和 warehouse#18。在 #3753 中,我们提出可以参考 You don't (may not) need Lodash/Underscore 逐步将 Lodash 替换为 Native JavaScript。经过 #3785、#3786、#3788、#3790、#3791、#3809、#3810、#3813、#3826、#3845、hexo-util#141、#3880、#3969 这一系列 PR,我们从 Hexo 中去除了 Lodash。我们也在 You-Dont-Need-Lodash-Underscore
提交了 PR、将我们的 _.assignIn
的 Native 替代方案带给社区。
缓存实用函数的返回值
Hexo 的 hexo-util
中有许多的实用函数,如计算相对路径的 relative_url(from, to)
、补全相对路径为 URL 的 url_for(path)
和 full_url_for(path)
、从 EMail 计算 gravatar URL 的 gravatar(mail)
、判断一个链接是否为外部链接的 isExternalLink(url)
。在 测试 中我们发现在 Hexo 生成过程中这些函数可能会被调用上千次,但在大部分调用中传入的参数都是相同的,因此可以把传入的参数和函数的返回值的键值对缓存起来。这一构想在 hexo-util#162 中被实现。
未来
在 #3776 中我们将 Benchmark 作为单元测试的一部分添加到 CI。之后 Benchmark 多次帮助我们找到潜在的性能问题(#3807、#3833),避免了 #3129 那样造成严重的性能下降的情况再次发生。而在 #4000 中我们计划进一步将生成火焰图也作为单元测试的一部分,帮助我们继续优化 Hexo 的生成过程。对于 Hexo 来说,速度一直都是关键。