速度就是关键! —— 我们是如何让 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 来说,速度一直都是关键。