Roosevelt

Teddy — Benchmarks

🔝 Scroll to top

Templating engine benchmarks

These benchmarks were ran on the following hardware:

Nodev26.9.0 (V8 14.6.202.34-node.32)
PlatformLinux 7.0.0-38-generic x64
CPUAMD Ryzen 9 9900X 12-Core Processor, 24 cores
Memory121 GB
Time per measurement1000 ms
Process isolationone child process per measurement

Overall

In Node, cached

Geometric mean of the per scenario ratios to Teddy.

EngineScenariosvs Teddy
Teddy6same
Pug61.27× slower
Eta61.29× slower
Squirrelly61.38× slower
Marko61.98× slower
doT62.69× slower
art-template62.99× slower
Dust.js65.80× slower
Handlebars66.53× slower
Lodash template67.46× slower
Mustache69.49× slower
Nunjucks69.79× slower
EJS617.62× slower
PHP (node-php-runner)629.64× slower
LiquidJS684.13× slower

In Node, cold

Geometric mean of the per scenario ratios to Teddy.

EngineScenariosvs Teddy
doT615.85× faster
Eta612.87× faster
Squirrelly69.79× faster
art-template63.92× faster
Mustache63.90× faster
Lodash template63.46× faster
EJS63.19× faster
Handlebars61.50× faster
Nunjucks61.45× faster
Dust.js61.34× faster
Teddy6same
LiquidJS61.01× slower
PHP (node-php-runner)61.71× slower
Pug62.88× slower
Marko66.35× slower

In a browser, cached

Ratios to Teddy (emitted js). Teddy is listed twice because it is the only engine here that renders two different ways: from emitted JavaScript, or by walking a node tree with no build step. The detail below says more about both.

EngineScenariosvs Teddy
Teddy (emitted js)6same
Pug61.21× slower
Eta61.25× slower
Squirrelly61.40× slower
doT61.87× slower
Marko62.44× slower
art-template62.46× slower
Dust.js64.01× slower
Handlebars65.31× slower
Mustache66.28× slower
Teddy (tree walk)66.30× slower
Lodash template66.56× slower
Nunjucks67.25× slower
EJS613.90× slower
LiquidJS6184.77× slower
PHP (node-php-runner)0cannot run client-side

In a browser, cold

Geometric mean of the per scenario ratios to Teddy.

EngineScenariosvs Teddy
doT616.73× faster
Eta610.87× faster
Squirrelly67.45× faster
Lodash template65.88× faster
art-template63.91× faster
EJS63.34× faster
Mustache63.10× faster
Nunjucks61.79× faster
Dust.js61.33× faster
Handlebars61.28× faster
Teddy6same
LiquidJS62.60× slower
Pug0compiles ahead of time
Marko0compiles ahead of time
PHP (node-php-runner)0cannot run client-side

cached (render only, template already compiled)

Variables

28 interpolations, one of them escaped html and one of them raw html. Measures the cost of looking a value up in the model and writing it out.

EngineVersionRenders/secMean ms± %vs TeddyOutput bytes
art-template4.13.41,231,1518.2e-40.11.64× faster1,259
Teddy2.2.0750,5251.4e-30.2same1,258
Eta4.6.0631,0381.6e-30.21.19× slower1,258
Pug3.0.4611,2261.7e-30.21.23× slower1,158
Squirrelly9.1.1572,0921.8e-30.21.31× slower1,258
Marko6.3.51538,2681.9e-30.21.39× slower1,140
doT1.1.3392,5452.6e-30.21.91× slower1,275
Handlebars4.7.9350,3182.9e-30.22.14× slower1,258
Nunjucks3.2.4286,7093.5e-30.22.62× slower1,258
Lodash template4.18.1240,3444.2e-30.13.12× slower1,258
Dust.js3.0.1240,1634.2e-30.23.13× slower1,258
Mustache4.2.0161,6276.3e-30.24.64× slower1,278
EJS6.0.1125,9728.0e-30.15.96× slower1,258
PHP (node-php-runner)2.0.042,7010.0240.317.58× slower1,258
LiquidJS10.29.026,9870.0370.227.81× slower1,258

Conditionals

8 branches: if/else, an else-if chain, a negation, a boolean pair, and an inline attribute condition. Measures the cost of evaluating a branch and discarding the branch not taken.

EngineVersionRenders/secMean ms± %vs TeddyOutput bytes
Squirrelly9.1.110,259,1311.0e-40.21.45× faster478
Eta4.6.09,973,9021.1e-40.31.41× faster478
art-template4.13.49,926,9621.1e-40.31.40× faster492
Pug3.0.49,438,5381.1e-40.51.33× faster410
Teddy2.2.07,080,8791.5e-40.8same516
doT1.1.35,285,7892.0e-40.61.34× slower492
Marko6.3.511,578,9486.7e-40.94.48× slower392
Nunjucks3.2.41,131,3959.1e-40.26.26× slower492
Dust.js3.0.1926,3011.1e-30.37.64× slower516
Mustache4.2.0521,3902.0e-30.413.58× slower454
Lodash template4.18.1514,0582.0e-30.113.77× slower492
Handlebars4.7.9481,3902.2e-30.614.71× slower453
EJS6.0.1374,4342.8e-30.518.91× slower492
LiquidJS10.29.065,4090.0160.5108.26× slower492
PHP (node-php-runner)2.0.064,9060.0170.8109.09× slower478

Loops

24 products, each with a branch and a nested loop over 3 tags. Measures nested iteration with per-iteration branching.

EngineVersionRenders/secMean ms± %vs TeddyOutput bytes
Teddy2.2.0295,5763.5e-30.3same11,795
art-template4.13.4242,2734.3e-30.31.22× slower11,627
Eta4.6.0231,6154.5e-30.31.28× slower11,425
Pug3.0.4201,9975.1e-30.41.46× slower7,782
Squirrelly9.1.1180,3435.7e-30.21.64× slower11,425
Marko6.3.51159,0876.5e-30.41.86× slower7,474
doT1.1.382,4440.0120.33.59× slower11,627
Handlebars4.7.945,9690.0220.36.43× slower10,121
Dust.js3.0.142,4450.0240.36.96× slower11,627
Lodash template4.18.136,5790.0280.28.08× slower11,627
Mustache4.2.033,5390.0300.38.81× slower10,121
EJS6.0.116,9940.0590.317.39× slower11,627
Nunjucks3.2.416,7350.0610.317.66× slower11,627
PHP (node-php-runner)2.0.011,4460.0920.525.82× slower11,425
LiquidJS10.29.02,6150.3860.4113.03× slower11,627

Large table

1,000 rows of 7 cells, one of them conditional. Measures how the engine scales when the same small body is rendered many times.

EngineVersionRenders/secMean ms± %vs TeddyOutput bytes
Teddy2.2.09,9050.1050.7same238,758
Eta4.6.07,8510.1300.71.26× slower237,757
art-template4.13.47,3660.1380.41.34× slower238,758
Marko6.3.516,9040.1470.51.43× slower156,732
Pug3.0.46,5370.1570.61.52× slower156,734
Squirrelly9.1.16,4780.1570.41.53× slower237,757
doT1.1.32,6470.3850.73.74× slower238,758
Handlebars4.7.92,3740.4310.84.17× slower233,753
Lodash template4.18.11,7330.5790.35.72× slower238,758
Dust.js3.0.11,7130.5870.45.78× slower238,758
Mustache4.2.01,1980.8440.78.27× slower233,753
Nunjucks3.2.41,1480.8740.48.63× slower238,758
EJS6.0.16581.530.815.05× slower238,758
PHP (node-php-runner)2.0.05531.841.517.90× slower237,757
LiquidJS10.29.01049.671.095.58× slower238,758

Partials

a shell that pulls in a header and a footer once and a product partial 24 times. Measures the per-call overhead of including another template.

EngineVersionRenders/secMean ms± %vs TeddyOutput bytes
Teddy2.2.0221,3174.6e-30.2same10,958
Pug3.0.4149,2566.9e-30.31.48× slower8,611
Squirrelly9.1.1119,4168.6e-30.31.85× slower10,672
Eta4.6.0110,4819.3e-30.32.00× slower10,672
Marko6.3.51109,4839.4e-30.32.02× slower8,247
doT1.1.365,4250.0160.33.38× slower10,931
Dust.js3.0.134,4760.0300.36.42× slower10,883
Lodash template4.18.121,8630.0470.310.12× slower10,883
Handlebars4.7.920,2370.0510.310.94× slower11,648
Mustache4.2.015,9830.0640.313.85× slower11,708
Nunjucks3.2.49,8990.12512.722.36× slower10,883
PHP (node-php-runner)2.0.07,5680.1361.129.24× slower10,672
EJS6.0.16,1820.1630.235.80× slower10,883
art-template4.13.45,7670.1821.638.38× slower10,883
LiquidJS10.29.02,0170.5050.7109.72× slower10,883

Full page

a complete document: head, header, banner, else-if chain, 24 products, a 25 row table, footer. Measures everything above at once, which is the closest thing here to a real request.

EngineVersionRenders/secMean ms± %vs TeddyOutput bytes
Teddy2.2.0147,2727.1e-30.3same17,397
Pug3.0.4104,6689.7e-30.21.41× slower11,824
Squirrelly9.1.191,2900.0110.21.61× slower17,067
Eta4.6.086,9130.0120.31.69× slower17,067
Marko6.3.5183,3050.0120.21.77× slower11,438
doT1.1.345,0600.0230.23.27× slower17,364
Dust.js3.0.123,9410.0420.26.15× slower17,328
Handlebars4.7.917,4950.0580.38.42× slower18,765
Lodash template4.18.117,2520.0580.28.54× slower17,308
Mustache4.2.012,8420.0790.311.47× slower18,837
Nunjucks3.2.49,3600.1263.515.73× slower17,308
art-template4.13.45,6980.1861.725.84× slower17,308
PHP (node-php-runner)2.0.05,6300.1840.626.16× slower17,067
EJS6.0.15,2040.1950.528.30× slower17,308
LiquidJS10.29.01,4820.6810.799.39× slower17,308

cold (compile and render together, engine cache off)

Variables

28 interpolations, one of them escaped html and one of them raw html. Measures the cost of looking a value up in the model and writing it out.

EngineVersionRenders/secMean ms± %vs TeddyOutput bytes
doT1.1.340,8040.0250.313.21× faster1,275
Eta4.6.020,4410.0500.36.62× faster1,258
EJS6.0.118,5960.0540.26.02× faster1,258
Squirrelly9.1.115,5140.0660.35.02× faster1,258
art-template4.13.411,7270.0880.83.80× faster1,259
Mustache4.2.09,2620.1090.23.00× faster1,278
Lodash template4.18.18,1870.1301.82.65× faster1,258
LiquidJS10.29.07,0290.1491.32.28× faster1,258
Nunjucks3.2.46,4680.1600.62.09× faster1,258
Teddy2.2.03,0880.3330.9same1,258
Handlebars4.7.92,4270.4180.61.27× slower1,258
Dust.js3.0.11,8180.5630.91.70× slower1,258
PHP (node-php-runner)2.0.09841.111.43.14× slower1,258
Pug3.0.43942.561.17.84× slower1,158
Marko6.3.513013.564.310.27× slower1,140

Conditionals

8 branches: if/else, an else-if chain, a negation, a boolean pair, and an inline attribute condition. Measures the cost of evaluating a branch and discarding the branch not taken.

EngineVersionRenders/secMean ms± %vs TeddyOutput bytes
doT1.1.351,6770.0200.453.61× faster492
EJS6.0.124,3170.0420.325.23× faster492
Eta4.6.022,4490.0450.323.29× faster478
Squirrelly9.1.116,1860.0630.416.79× faster478
art-template4.13.416,0930.0650.816.70× faster492
LiquidJS10.29.011,7880.0922.012.23× faster492
Lodash template4.18.18,8790.1211.69.21× faster492
Mustache4.2.08,3780.1220.48.69× faster454
Nunjucks3.2.45,5900.1820.45.80× faster492
Handlebars4.7.92,4480.4130.52.54× faster453
Dust.js3.0.11,8130.5570.61.88× faster516
Teddy2.2.09641.071.7same516
PHP (node-php-runner)2.0.09081.151.21.06× slower478
Pug3.0.47091.420.71.36× slower410
Marko6.3.511835.754.45.27× slower392

Loops

24 products, each with a branch and a nested loop over 3 tags. Measures nested iteration with per-iteration branching.

EngineVersionRenders/secMean ms± %vs TeddyOutput bytes
doT1.1.334,1880.0300.315.96× faster11,627
Eta4.6.023,0870.0440.310.77× faster11,425
art-template4.13.421,2840.0490.89.93× faster11,627
Squirrelly9.1.116,4770.0620.47.69× faster11,425
EJS6.0.111,8200.0860.35.52× faster11,627
Mustache4.2.09,0340.1120.34.22× faster10,121
Lodash template4.18.16,5160.1621.53.04× faster11,627
Nunjucks3.2.45,5560.1830.52.59× faster11,627
Handlebars4.7.93,7720.2740.81.76× faster10,121
Dust.js3.0.12,3540.4340.71.10× faster11,627
LiquidJS10.29.02,1510.4730.7same11,627
Teddy2.2.02,1430.4861.3same11,795
PHP (node-php-runner)2.0.08841.161.02.42× slower11,425
Pug3.0.46671.521.13.21× slower7,782
Marko6.3.513832.814.55.59× slower7,474

Large table

1,000 rows of 7 cells, one of them conditional. Measures how the engine scales when the same small body is rendered many times.

EngineVersionRenders/secMean ms± %vs TeddyOutput bytes
Eta4.6.05,8770.1740.62.59× faster237,757
art-template4.13.45,8280.1770.72.57× faster238,758
Squirrelly9.1.14,7580.2150.62.10× faster237,757
doT1.1.32,5860.3910.51.14× faster238,758
Teddy2.2.02,2670.4651.3same238,758
Handlebars4.7.91,7300.5930.91.31× slower233,753
Mustache4.2.01,2150.8340.81.87× slower233,753
Lodash template4.18.11,1241.3314.52.02× slower238,758
Nunjucks3.2.41,0280.9770.52.20× slower238,758
Dust.js3.0.19251.100.92.45× slower238,758
Pug3.0.46901.471.13.28× slower156,734
EJS6.0.16471.561.03.50× slower238,758
PHP (node-php-runner)2.0.05401.891.44.20× slower237,757
Marko6.3.514792.243.64.73× slower156,732
LiquidJS10.29.097.310.331.523.30× slower238,758

Partials

a shell that pulls in a header and a footer once and a product partial 24 times. Measures the per-call overhead of including another template.

EngineVersionRenders/secMean ms± %vs TeddyOutput bytes
doT1.1.325,3660.0400.225.57× faster10,931
Eta4.6.022,2820.0460.322.46× faster10,672
Squirrelly9.1.117,8240.0570.417.97× faster10,672
Mustache4.2.05,3970.1870.45.44× faster11,708
Lodash template4.18.14,6890.2191.34.73× faster10,883
Dust.js3.0.12,8050.3620.62.83× faster10,883
Handlebars4.7.91,5460.6661.11.56× faster11,648
EJS6.0.11,2780.7900.61.29× faster10,883
Teddy2.2.09921.051.7same10,958
art-template4.13.49401.081.21.06× slower10,883
PHP (node-php-runner)2.0.06871.481.11.44× slower10,672
LiquidJS10.29.05701.770.71.74× slower10,883
Nunjucks3.2.44442.281.22.24× slower10,883
Pug3.0.43562.820.72.79× slower8,611
Marko6.3.5110110.384.99.86× slower8,247

Full page

a complete document: head, header, banner, else-if chain, 24 products, a 25 row table, footer. Measures everything above at once, which is the closest thing here to a real request.

EngineVersionRenders/secMean ms± %vs TeddyOutput bytes
doT1.1.316,5400.0620.348.11× faster17,364
Eta4.6.016,1170.0640.446.88× faster17,067
Squirrelly9.1.112,3480.0830.435.92× faster17,067
Mustache4.2.03,7540.2690.410.92× faster18,837
Lodash template4.18.13,3600.3071.19.77× faster17,308
Dust.js3.0.11,4180.7140.74.12× faster17,328
EJS6.0.11,1730.8590.63.41× faster17,308
Handlebars4.7.99271.090.82.70× faster18,765
art-template4.13.48191.261.52.38× faster17,308
PHP (node-php-runner)2.0.06771.501.01.97× faster17,067
Nunjucks3.2.44932.071.41.43× faster17,308
LiquidJS10.29.04672.171.11.36× faster17,308
Teddy2.2.03442.961.9same17,397
Pug3.0.41895.310.71.82× slower11,824
Marko6.3.5173.813.944.44.66× slower11,438

In a browser, cached (chromium)

The same scenarios rendered inside a real browser rather than in Node, timing rendering only. A browser has no filesystem, so every template and partial is handed to the engine up front.

Teddy is listed twice, and it is the only engine here that is. Every other engine in this table renders one way: it turns a template into a JavaScript function and calls it. Teddy can do that, and it can also render by walking the node tree its compiler built, without writing any JavaScript at all. Those are two different renderers, not two ways of arriving at the same one, which is why the route changes the number for Teddy and for nobody else. Both rows are real ways to run it, and which one an app gets depends on whether it has a build step:

  • Teddy (emitted js) is the emitted JavaScript, written out by teddy.precompile() in Node and loaded as a script. Faster, and it needs a build step and a known set of templates.
  • Teddy (tree walk) is what a page gets with no build step at all: drop in the browser build and render. It is also the only thing that can render a template that does not exist until runtime, which is what a live editor or a template fetched from a server needs.

Ratios are against Teddy (emitted js), since that is the like for like against the other engines here, all of which are running a compiled function too.

Variables

28 interpolations, one of them escaped html and one of them raw html. Measures the cost of looking a value up in the model and writing it out.

EngineCompiledRenders/secMean msvs TeddyOutput bytes
art-templatein page1,096,9839.1e-41.44× faster1,259
Pugahead of time839,3391.2e-31.10× faster1,158
Teddy (emitted js)ahead of time762,6861.3e-3same1,258
Etain page742,7991.3e-31.03× slower1,258
Squirrellyin page687,9001.5e-31.11× slower1,258
doTin page539,9361.9e-31.41× slower1,275
Teddy (tree walk)in page415,5632.4e-31.84× slower1,258
Markoahead of time408,6672.4e-31.87× slower1,140
Handlebarsin page398,4872.5e-31.91× slower1,258
Nunjucksin page335,4023.0e-32.27× slower1,258
Dust.jsin page305,7333.3e-32.49× slower1,258
Lodash templatein page273,6633.7e-32.79× slower1,258
Mustachein page242,2504.1e-33.15× slower1,278
EJSin page142,1547.0e-35.37× slower1,258
LiquidJSin page13,4960.07456.51× slower1,258
PHP (node-php-runner)———cannot run client-side—

Conditionals

8 branches: if/else, an else-if chain, a negation, a boolean pair, and an inline attribute condition. Measures the cost of evaluating a branch and discarding the branch not taken.

EngineCompiledRenders/secMean msvs TeddyOutput bytes
art-templatein page5,728,1171.7e-41.39× faster492
Etain page5,545,1211.8e-41.35× faster478
Squirrellyin page5,528,4001.8e-41.34× faster478
Pugahead of time5,418,0781.8e-41.32× faster410
doTin page4,691,7692.1e-41.14× faster492
Teddy (emitted js)ahead of time4,119,9842.4e-4same516
Nunjucksin page1,095,6369.1e-43.76× slower492
Dust.jsin page972,3401.0e-34.24× slower516
Teddy (tree walk)in page700,2951.4e-35.88× slower516
Markoahead of time695,4181.4e-35.92× slower392
Mustachein page576,6661.7e-37.14× slower454
Lodash templatein page549,4101.8e-37.50× slower492
Handlebarsin page518,8401.9e-37.94× slower453
EJSin page395,9962.5e-310.40× slower492
LiquidJSin page30,6940.033134.23× slower492
PHP (node-php-runner)———cannot run client-side—

Loops

24 products, each with a branch and a nested loop over 3 tags. Measures nested iteration with per-iteration branching.

EngineCompiledRenders/secMean msvs TeddyOutput bytes
Teddy (emitted js)ahead of time326,9843.1e-3same11,795
art-templatein page269,4053.7e-31.21× slower11,627
Etain page261,4893.8e-31.25× slower11,425
Pugahead of time216,1234.6e-31.51× slower7,782
Squirrellyin page182,2555.5e-31.79× slower11,425
Markoahead of time132,7027.5e-32.46× slower7,474
doTin page123,5178.1e-32.65× slower11,627
Dust.jsin page63,8350.0165.12× slower11,627
Handlebarsin page52,5710.0196.22× slower10,121
Mustachein page51,3600.0196.37× slower10,121
Lodash templatein page37,6840.0278.68× slower11,627
Nunjucksin page24,8580.04013.15× slower11,627
Teddy (tree walk)in page24,3700.04113.42× slower11,795
EJSin page18,8240.05317.37× slower11,627
LiquidJSin page1,2660.790258.21× slower11,627
PHP (node-php-runner)———cannot run client-side—

Large table

1,000 rows of 7 cells, one of them conditional. Measures how the engine scales when the same small body is rendered many times.

EngineCompiledRenders/secMean msvs TeddyOutput bytes
Teddy (emitted js)ahead of time10,2100.098same238,758
Etain page8,3210.1201.23× slower237,757
art-templatein page7,4870.1341.36× slower238,758
Pugahead of time6,8830.1451.48× slower156,734
Squirrellyin page6,5610.1521.56× slower237,757
Markoahead of time6,5420.1531.56× slower156,732
doTin page4,0330.2482.53× slower238,758
Handlebarsin page3,0550.3273.34× slower233,753
Dust.jsin page2,5720.3893.97× slower238,758
Teddy (tree walk)in page1,9620.5105.20× slower238,758
Mustachein page1,8650.5365.47× slower233,753
Lodash templatein page1,8450.5425.53× slower238,758
Nunjucksin page1,6960.5906.02× slower238,758
EJSin page7641.3113.37× slower238,758
LiquidJSin page49.720.12205.40× slower238,758
PHP (node-php-runner)———cannot run client-side—

Partials

a shell that pulls in a header and a footer once and a product partial 24 times. Measures the per-call overhead of including another template.

EngineCompiledRenders/secMean msvs TeddyOutput bytes
Teddy (emitted js)ahead of time238,4524.2e-3same10,958
Pugahead of time162,3486.2e-31.47× slower8,611
Squirrellyin page127,7487.8e-31.87× slower10,672
Etain page117,8238.5e-32.02× slower10,672
doTin page100,3551.0e-22.38× slower10,931
Markoahead of time98,3500.0102.42× slower8,247
Dust.jsin page52,6260.0194.53× slower10,883
Handlebarsin page25,4650.0399.36× slower11,648
Mustachein page25,3140.0409.42× slower11,708
Lodash templatein page24,6200.0419.69× slower10,883
Teddy (tree walk)in page23,7270.04210.05× slower10,958
Nunjucksin page14,2330.07016.75× slower10,883
art-templatein page11,8610.08420.10× slower10,883
EJSin page9,0770.11026.27× slower10,883
LiquidJSin page7001.43340.43× slower10,883
PHP (node-php-runner)———cannot run client-side—

Full page

a complete document: head, header, banner, else-if chain, 24 products, a 25 row table, footer. Measures everything above at once, which is the closest thing here to a real request.

EngineCompiledRenders/secMean msvs TeddyOutput bytes
Teddy (emitted js)ahead of time153,1046.5e-3same17,397
Pugahead of time111,7079.0e-31.37× slower11,824
Etain page94,4050.0111.62× slower17,067
Squirrellyin page87,3750.0111.75× slower17,067
Markoahead of time73,9850.0142.07× slower11,438
doTin page69,7950.0142.19× slower17,364
Dust.jsin page35,6300.0284.30× slower17,328
Handlebarsin page20,3110.0497.54× slower18,765
Lodash templatein page18,6750.0548.20× slower17,308
Teddy (tree walk)in page18,5360.0548.26× slower17,397
Mustachein page18,3450.0558.35× slower18,837
Nunjucksin page11,9960.08312.76× slower17,308
art-templatein page11,5950.08613.20× slower17,308
EJSin page7,2310.13821.17× slower17,308
LiquidJSin page5271.90290.55× slower17,308
PHP (node-php-runner)———cannot run client-side—

Engines with nothing a browser can run:

  • PHP (node-php-runner): not a javascript engine, so there is nothing a browser can run either way

In a browser, cold (chromium)

The same scenarios in a browser, timing the compile and one render together, with whatever the engine kept from last time dropped first. This is the browser's answer to the cold mode above: what the first render of a template costs. Only engines that can compile in a browser at all appear, which is what leaves Pug and Marko out of it.

Variables

28 interpolations, one of them escaped html and one of them raw html. Measures the cost of looking a value up in the model and writing it out.

EngineRenders/secMean msvs TeddyOutput bytes
doT92,3490.01116.18× faster1,275
Eta34,5800.0296.06× faster1,258
Lodash template27,7820.0364.87× faster1,258
EJS26,9070.0374.71× faster1,258
Squirrelly23,6600.0424.14× faster1,258
Mustache13,8680.0722.43× faster1,278
art-template13,4480.0742.36× faster1,259
Nunjucks7,3000.1371.28× faster1,258
Teddy5,7090.175same1,258
LiquidJS5,2410.1911.09× slower1,258
Handlebars4,2850.2331.33× slower1,258
Dust.js3,3130.3021.72× slower1,258
Pug——compiles ahead of time—
Marko——compiles ahead of time—
PHP (node-php-runner)——cannot run client-side—

Conditionals

8 branches: if/else, an else-if chain, a negation, a boolean pair, and an inline attribute condition. Measures the cost of evaluating a branch and discarding the branch not taken.

EngineRenders/secMean msvs TeddyOutput bytes
doT140,9287.1e-350.78× faster492
Lodash template41,5510.02414.97× faster492
Eta41,3620.02414.91× faster478
EJS38,5680.02613.90× faster492
Squirrelly25,8360.0399.31× faster478
art-template19,5960.0517.06× faster492
Mustache13,6270.0734.91× faster454
Nunjucks8,5910.1163.10× faster492
LiquidJS8,5730.1173.09× faster492
Handlebars4,1670.2401.50× faster453
Dust.js3,3700.2971.21× faster516
Teddy2,7750.360same516
Pug——compiles ahead of time—
Marko——compiles ahead of time—
PHP (node-php-runner)——cannot run client-side—

Loops

24 products, each with a branch and a nested loop over 3 tags. Measures nested iteration with per-iteration branching.

EngineRenders/secMean msvs TeddyOutput bytes
doT76,0300.01316.94× faster11,627
Eta41,1010.0249.16× faster11,425
art-template26,1200.0385.82× faster11,627
Squirrelly26,0050.0385.79× faster11,425
Lodash template22,0580.0454.91× faster11,627
EJS14,6340.0683.26× faster11,627
Mustache14,5800.0693.25× faster10,121
Nunjucks7,9280.1261.77× faster11,627
Handlebars6,5110.1541.45× faster10,121
Dust.js4,4980.222same11,627
Teddy4,4890.223same11,795
LiquidJS1,1120.9004.04× slower11,627
Pug——compiles ahead of time—
Marko——compiles ahead of time—
PHP (node-php-runner)——cannot run client-side—

Large table

1,000 rows of 7 cells, one of them conditional. Measures how the engine scales when the same small body is rendered many times.

EngineRenders/secMean msvs TeddyOutput bytes
Eta7,1270.1404.54× faster237,757
art-template6,0570.1653.86× faster238,758
Squirrelly5,4350.1843.46× faster237,757
doT3,8910.2572.48× faster238,758
Handlebars2,2890.4371.46× faster233,753
Lodash template1,7760.5631.13× faster238,758
Dust.js1,7300.5781.10× faster238,758
Mustache1,7180.5821.10× faster233,753
Teddy1,5690.637same238,758
Nunjucks1,5400.6491.02× slower238,758
EJS7511.332.09× slower238,758
LiquidJS49.220.3231.89× slower238,758
Pug——compiles ahead of time—
Marko——compiles ahead of time—
PHP (node-php-runner)——cannot run client-side—

Partials

a shell that pulls in a header and a footer once and a product partial 24 times. Measures the per-call overhead of including another template.

EngineRenders/secMean msvs TeddyOutput bytes
doT47,1640.02118.55× faster10,931
Eta38,7350.02615.24× faster10,672
Squirrelly27,3700.03710.77× faster10,672
Lodash template17,1740.0586.76× faster10,883
Mustache8,6720.1153.41× faster11,708
EJS6,4290.1562.53× faster10,883
Dust.js5,4960.1822.16× faster10,883
art-template5,3140.1882.09× faster10,883
Nunjucks4,0480.2471.59× faster10,883
Handlebars2,7540.3631.08× faster11,648
Teddy2,5420.393same10,958
LiquidJS6571.523.87× slower10,883
Pug——compiles ahead of time—
Marko——compiles ahead of time—
PHP (node-php-runner)——cannot run client-side—

Full page

a complete document: head, header, banner, else-if chain, 24 products, a 25 row table, footer. Measures everything above at once, which is the closest thing here to a real request.

EngineRenders/secMean msvs TeddyOutput bytes
doT28,4670.03534.26× faster17,364
Eta23,8930.04228.76× faster17,067
Squirrelly17,0550.05920.53× faster17,067
Lodash template12,5130.08015.06× faster17,308
Mustache5,1320.1956.18× faster18,837
EJS4,4930.2235.41× faster17,308
art-template3,7820.2644.55× faster17,308
Dust.js2,7020.3703.25× faster17,328
Nunjucks2,4690.4052.97× faster17,308
Handlebars1,4020.7131.69× faster18,765
Teddy8311.20same17,397
LiquidJS4722.121.76× slower17,308
Pug——compiles ahead of time—
Marko——compiles ahead of time—
PHP (node-php-runner)——cannot run client-side—

Engines with nothing a browser can run:

  • Pug: ships no build that compiles a template in a browser: pug compiles ahead of time, so it is measured that way
  • Marko: ships no build that compiles a template in a browser: marko compiles ahead of time
  • PHP (node-php-runner): not a javascript engine, so there is nothing to run in a browser

Notes on individual engines

  • Pug: partials are mixins; pug include is inlined at compile time
  • Mustache: logic-less: else branches are written as inverted sections
  • Dust.js: renders through a callback, called before it returns
  • Marko: ahead-of-time compiler; a .marko file becomes a js module
  • LiquidJS: output is unescaped by default; escaping is explicit
  • doT: partials are compile-time definitions, inlined before the render function exists
  • Lodash template: no include mechanism; partials are passed in as compiled functions
  • PHP (node-php-runner): not a javascript engine: the template is PHP, and every render crosses into another process, so a fixed round trip and writing the part of the model the page reads as JSON cost more than running the template does