Speed is the Key - How We Make Hexo 30% Faster

Originally written by Sukka (one of the current Hexo development team member) in Simplified Chinese and translated into English by himself.

Speed is always the key to Hexo. 3 years ago, Hexo 3.2 speeds up generation speed by 2x through templates precompilation. And we are now at Hexo 4.2, with several performance improvements we successfully make the generation speed 30% faster compared to Hexo 3.2.

Benchmark

Here is how the benchmark being set up:

  • Travis CI - Ubuntu Xenial 16.04
    • CPU:2 Cores
    • RAM:7.5 GB
  • Hexo default’s theme: landscape
  • 300 randomly generated posts. Each post contains all commonly used Markdown syntaxs and the code blocks for testing highlight.js. A unique category and three tags are also set in Front Matter of those posts.

Since Hexo 3.2, rendered contents will be cached in the warehouse (db.json), therefore the performance of both cold generation (hexo clean before hexo g) and hot generation (no hexo clean) are tested in the benchmark. Each benchmark is performed by Cold => Hot => Cold. Memory usage is measured using time and the value of Resident Set Size (RSS) will be taken.

You can find the benchmark script here.

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%

Drop cheerio dependency from Hexo

As you can see through benchmark result, there is a serious performance regression in Hexo 3.8. It turns out that the meta_generator filter introduced in #3129 is the culprit. #3129 uses cheerio to insert <meta name = "generator" content = "Hexo [version]"> into <head>, thus cheerio has to load all the HTMLs generated by Hexo into memory and parsed into DOM.

cheerio is fast, but still there will be performance bottlenecks when traversing through hundreds of HTML files. In #3677 we made a proposal to relpace cheerio with native API. In #3671, #3680 and #3685 we replace cheerio wirh regex for open_graph() helper, meta_generator filter and external_link filter, and in hexo-util#137 & #3850 we replace cheerio with faster htmlparser2. Now we have completely dropped cheerio in Hexo 4.2

Improve Cache of Rendered HTML mechanism

Cache of Rendered HTML is introduced in Hexo 3.0.0-rc4 (e8e45ed), which is an attempt to improve Hexo’s generation performance by caching rendering results. However, each route is used only once during hexo g, so memory is consumed while no performance gained. In #3756 Cache of Rendered HTML is disabled for hexo g and enabled for hexo s, so the the memory usage of hexo g has been reduced.

Drop Lodash dependency from Hexo

Lodash is a modern JavaScript utility library that makes working with arrays, numbers, objects and strings much easier. However, as more and more new features have been brought to ES6, most of Lodash’s features could be replaced by native JavaScript.

Hexo actually started to reduce Lodash dependent a year ago, such as #3285, #3290 & warehouse#18. In #3753, we propose to gradually replace Lodash with native JavaScript by following the You don’t (may not) need Lodash/Underscore. After #3785, #3786, #3788, #3790, #3791, #3809, #3810, #3813, #3826, #3845, hexo-util#141, #3880 & #3969, we successfully dropped Lodash from Hexo. We also opened a new PR at You don’t (may not) need Lodash/Underscore to bring our _.assignIn alternative back to the community.

Cache the return value of utility function

There are many utilities in hexo-util, such as relative_url(from, to) for calculating relative paths, url_for(path) and full_url_for(path) for trasnsforming relative paths into URLs, gravatar(mail) for calculating gravatar URLs from E-mail address, and isExternalLink(url) for determine whether the given URL is an external link. We found out that those functions might be called thousands of times during the Hexo generation process while the same parameters might be passed repeatedly, so the key-values of the parameters and the return value could be cached. This idea was implemented in hexo-util#162.

Future

We have added Benchmark to CI as part of the unit test in #3776. Since then, benchmark has helped us find potential performance regressions (likes #3807 & #3833) several times and avoiding the severe performance regression like #3129. And we are going to take a step further to add flamegraph to unit case in #4000, which will help us better optimizing the generation process of Hexo. For Hexo, speed is always the key.