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.29× slower
Eta61.31× slower
Squirrelly61.39× slower
Marko61.96× slower
doT62.77× slower
art-template63.07× slower
Dust.js65.94× slower
Handlebars66.42× slower
Lodash template67.60× slower
Mustache69.14× slower
Nunjucks69.69× slower
EJS617.63× slower
PHP (node-php-runner)627.76× slower
LiquidJS683.86× slower

In Node, cold

Geometric mean of the per scenario ratios to Teddy.

EngineScenariosvs Teddy
doT615.45× faster
Eta612.67× faster
Squirrelly69.65× faster
art-template63.85× faster
Mustache63.78× faster
Lodash template63.47× faster
EJS63.10× faster
Handlebars61.48× faster
Nunjucks61.44× faster
Dust.js61.35× faster
Teddy6same
LiquidJS61.02× slower
PHP (node-php-runner)61.03× slower
Pug62.87× slower
Marko66.24× 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
Pug61.31× faster
Eta61.26× faster
Squirrelly61.13× faster
Teddy (emitted js)6same
doT61.19× slower
Marko61.55× slower
art-template61.56× slower
Dust.js62.55× slower
Handlebars63.37× slower
Mustache63.98× slower
Lodash template64.16× slower
Teddy (tree walk)64.27× slower
Nunjucks64.56× slower
EJS68.80× slower
LiquidJS6116.68× slower
PHP (node-php-runner)0cannot run client-side

In a browser, cold

Geometric mean of the per scenario ratios to Teddy.

EngineScenariosvs Teddy
doT615.13× faster
Eta69.76× faster
Squirrelly66.73× faster
Lodash template65.25× faster
art-template63.49× faster
EJS62.97× faster
Mustache62.75× faster
Nunjucks61.56× faster
Dust.js61.19× faster
Handlebars61.13× faster
Teddy6same
LiquidJS62.91× 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,204,5798.4e-40.11.64× faster1,259
Teddy2.1.0732,7661.4e-30.2same1,258
Eta4.6.0650,3471.6e-30.11.13× slower1,258
Pug3.0.4613,9661.6e-30.11.19× slower1,158
Squirrelly9.1.1588,5691.7e-30.11.24× slower1,258
Marko6.3.51543,3491.9e-30.11.35× slower1,140
doT1.1.3392,8022.6e-30.11.87× slower1,275
Handlebars4.7.9352,6612.9e-30.12.08× slower1,258
Nunjucks3.2.4296,7203.4e-30.12.47× slower1,258
Dust.js3.0.1239,8614.2e-30.23.05× slower1,258
Lodash template4.18.1230,4934.4e-30.13.18× slower1,258
Mustache4.2.0167,9246.0e-30.14.36× slower1,278
EJS6.0.1124,4918.1e-30.15.89× slower1,258
PHP (node-php-runner)2.0.045,1950.0230.316.21× slower1,258
LiquidJS10.29.026,9280.0380.227.21× 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,206,1291.0e-40.11.42× faster478
Eta4.6.09,763,3681.1e-40.21.36× faster478
art-template4.13.49,602,3431.1e-40.21.34× faster492
Pug3.0.49,131,1171.2e-40.21.27× faster410
Teddy2.1.07,190,2711.5e-41.4same516
doT1.1.34,876,0322.1e-40.61.47× slower492
Marko6.3.511,700,3876.1e-40.84.23× slower392
Nunjucks3.2.41,172,0698.6e-40.16.13× slower492
Dust.js3.0.1939,2511.1e-30.27.66× slower516
Mustache4.2.0528,1731.9e-30.113.61× slower454
Lodash template4.18.1514,5422.0e-30.113.97× slower492
Handlebars4.7.9484,3842.1e-30.214.84× slower453
EJS6.0.1378,4992.7e-30.119.00× slower492
PHP (node-php-runner)2.0.069,0440.0150.3104.14× slower478
LiquidJS10.29.068,1920.0150.1105.44× slower492

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.1.0308,8773.3e-30.2same11,795
art-template4.13.4237,9034.3e-30.21.30× slower11,627
Eta4.6.0235,8934.4e-30.31.31× slower11,425
Pug3.0.4207,6514.9e-30.21.49× slower7,782
Squirrelly9.1.1180,3185.6e-30.21.71× slower11,425
Marko6.3.51164,1216.2e-30.21.88× slower7,474
doT1.1.382,0190.0120.23.77× slower11,627
Handlebars4.7.946,9630.0220.26.58× slower10,121
Dust.js3.0.139,2140.0260.17.88× slower11,627
Mustache4.2.038,0490.0260.18.12× slower10,121
Lodash template4.18.136,1790.0280.18.54× slower11,627
Nunjucks3.2.417,6130.0570.217.54× slower11,627
EJS6.0.117,5130.0570.117.64× slower11,627
PHP (node-php-runner)2.0.013,2410.0770.323.33× slower11,425
LiquidJS10.29.02,6780.3740.2115.34× 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.1.010,2170.1000.5same238,758
Eta4.6.07,9670.1270.31.28× slower237,757
art-template4.13.47,5350.1350.51.36× slower238,758
Marko6.3.516,9040.1460.31.48× slower156,732
Squirrelly9.1.16,5380.1540.31.56× slower237,757
Pug3.0.46,4970.1570.51.57× slower156,734
Handlebars4.7.92,7820.3640.53.67× slower233,753
doT1.1.32,6510.3810.53.85× slower238,758
Lodash template4.18.11,7470.5740.35.85× slower238,758
Dust.js3.0.11,7370.5770.25.88× slower238,758
Mustache4.2.01,3740.7310.47.44× slower233,753
Nunjucks3.2.41,1340.8840.39.01× slower238,758
EJS6.0.16841.460.414.94× slower238,758
PHP (node-php-runner)2.0.05971.711.617.11× slower237,757
LiquidJS10.29.01039.710.799.13× 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.1.0224,0834.5e-30.2same10,958
Pug3.0.4157,7236.4e-30.21.42× slower8,611
Squirrelly9.1.1124,3158.1e-30.11.80× slower10,672
Marko6.3.51112,8119.0e-30.21.99× slower8,247
Eta4.6.0109,2769.3e-30.22.05× slower10,672
doT1.1.365,9310.0150.23.40× slower10,931
Dust.js3.0.134,9590.0290.26.41× slower10,883
Lodash template4.18.122,7820.0440.19.84× slower10,883
Handlebars4.7.920,9490.0480.210.70× slower11,648
Mustache4.2.016,1450.0620.113.88× slower11,708
Nunjucks3.2.410,1180.1133.022.15× slower10,883
PHP (node-php-runner)2.0.08,3050.1210.226.98× slower10,672
EJS6.0.16,2650.1600.435.77× slower10,883
art-template4.13.45,8020.1801.438.62× slower10,883
LiquidJS10.29.02,1260.4730.4105.39× 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.1.0151,6356.7e-30.2same17,397
Pug3.0.4104,0369.8e-30.21.46× slower11,824
Squirrelly9.1.189,1370.0110.21.70× slower17,067
Eta4.6.085,6060.0120.21.77× slower17,067
Marko6.3.5184,8580.0120.21.79× slower11,438
doT1.1.345,5410.0220.23.33× slower17,364
Dust.js3.0.124,0760.0420.26.30× slower17,328
Lodash template4.18.117,2130.0580.18.81× slower17,308
Handlebars4.7.917,2070.0580.28.81× slower18,765
Mustache4.2.012,9890.0770.111.67× slower18,837
Nunjucks3.2.49,7380.1183.115.57× slower17,308
PHP (node-php-runner)2.0.06,0270.1700.625.16× slower17,067
art-template4.13.45,6540.1831.326.82× slower17,308
EJS6.0.15,3250.1880.128.48× slower17,308
LiquidJS10.29.01,5070.6660.3100.60× 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.341,1400.0250.213.52× faster1,275
Eta4.6.020,7370.0480.26.82× faster1,258
EJS6.0.118,5560.0540.26.10× faster1,258
Squirrelly9.1.115,4880.0650.25.09× faster1,258
art-template4.13.411,9690.0860.93.93× faster1,259
Mustache4.2.09,2770.1090.23.05× faster1,278
Lodash template4.18.18,6280.1201.42.84× faster1,258
LiquidJS10.29.07,0610.1471.02.32× faster1,258
Nunjucks3.2.46,6650.1510.32.19× faster1,258
Teddy2.1.03,0420.3411.1same1,258
Handlebars4.7.92,5030.4030.41.22× slower1,258
PHP (node-php-runner)2.0.02,3160.8762.31.31× slower1,258
Dust.js3.0.11,8780.5340.31.62× slower1,258
Pug3.0.44052.480.87.50× slower1,158
Marko6.3.513123.373.59.74× 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.352,5480.0190.253.58× faster492
EJS6.0.124,8370.0410.225.32× faster492
Eta4.6.023,5270.0430.223.99× faster478
Squirrelly9.1.116,6290.0600.216.96× faster478
art-template4.13.416,4780.0620.616.80× faster492
LiquidJS10.29.012,0050.0891.512.24× faster492
Lodash template4.18.19,5090.1091.59.70× faster492
Mustache4.2.08,4540.1190.38.62× faster454
Nunjucks3.2.45,8070.1740.35.92× faster492
PHP (node-php-runner)2.0.05,4270.6423.35.53× faster478
Handlebars4.7.92,4570.4120.52.51× faster453
Dust.js3.0.11,9160.5240.31.95× faster516
Teddy2.1.09811.041.4same516
Pug3.0.47241.390.51.35× slower410
Marko6.3.511965.303.45.01× 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,8050.0290.115.29× faster11,627
Eta4.6.023,5960.0430.210.36× faster11,425
art-template4.13.420,9770.0490.69.21× faster11,627
Squirrelly9.1.117,0710.0590.37.50× faster11,425
EJS6.0.111,7920.0850.25.18× faster11,627
Mustache4.2.09,0650.1110.13.98× faster10,121
Lodash template4.18.16,9300.1491.13.04× faster11,627
Nunjucks3.2.45,7810.1740.42.54× faster11,627
Handlebars4.7.93,8030.2700.71.67× faster10,121
Dust.js3.0.12,4680.4070.31.08× faster11,627
Teddy2.1.02,2770.4491.0same11,795
LiquidJS10.29.02,2140.4590.71.03× slower11,627
PHP (node-php-runner)2.0.01,2190.9631.21.87× slower11,425
Pug3.0.47171.400.53.18× slower7,782
Marko6.3.514032.664.45.65× 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,9290.1730.62.42× faster237,757
art-template4.13.45,8950.1740.62.40× faster238,758
Squirrelly9.1.14,8550.2090.51.98× faster237,757
doT1.1.32,6310.3830.51.07× faster238,758
Teddy2.1.02,4520.4201.0same238,758
Handlebars4.7.91,8140.5570.61.35× slower233,753
Mustache4.2.01,2570.8000.51.95× slower233,753
Lodash template4.18.11,1511.2713.42.13× slower238,758
Nunjucks3.2.41,0360.9680.32.37× slower238,758
Dust.js3.0.19631.040.62.55× slower238,758
Pug3.0.47411.360.63.31× slower156,734
EJS6.0.16331.580.43.88× slower238,758
PHP (node-php-runner)2.0.05341.912.24.59× slower237,757
Marko6.3.515142.053.04.77× slower156,732
LiquidJS10.29.01029.870.924.16× 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,3500.0400.224.11× faster10,931
Eta4.6.023,1700.0440.222.04× faster10,672
Squirrelly9.1.118,4470.0540.217.54× faster10,672
Mustache4.2.05,4200.1850.25.15× faster11,708
Lodash template4.18.14,7490.2161.24.52× faster10,883
Dust.js3.0.12,9030.3460.32.76× faster10,883
Handlebars4.7.91,6070.6280.61.53× faster11,648
EJS6.0.11,3270.7540.21.26× faster10,883
Teddy2.1.01,0510.9671.0same10,958
art-template4.13.49611.050.91.09× slower10,883
PHP (node-php-runner)2.0.08151.240.71.29× slower10,672
LiquidJS10.29.05791.730.41.82× slower10,883
Nunjucks3.2.44612.180.62.28× slower10,883
Pug3.0.43642.760.52.89× slower8,611
Marko6.3.511069.764.19.92× 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,8040.0600.247.51× faster17,364
Eta4.6.016,2010.0630.345.81× faster17,067
Squirrelly9.1.112,7010.0790.235.91× faster17,067
Mustache4.2.03,7390.2690.310.57× faster18,837
Lodash template4.18.13,4810.2910.89.84× faster17,308
Dust.js3.0.11,4950.6740.54.23× faster17,328
EJS6.0.11,2030.8330.33.40× faster17,308
Handlebars4.7.99431.070.52.67× faster18,765
art-template4.13.48561.191.12.42× faster17,308
PHP (node-php-runner)2.0.07911.270.52.24× faster17,067
Nunjucks3.2.45251.920.91.48× faster17,308
LiquidJS10.29.05021.990.41.42× faster17,308
Teddy2.1.03542.861.2same17,397
Pug3.0.41945.150.71.82× slower11,824
Marko6.3.5178.612.943.24.50× 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,116,9199.0e-42.06× faster1,259
Pugahead of time862,3981.2e-31.59× faster1,158
Etain page744,1331.3e-31.37× faster1,258
Squirrellyin page686,4391.5e-31.26× faster1,258
doTin page548,3511.8e-31.01× faster1,275
Teddy (emitted js)ahead of time543,5031.8e-3same1,258
Handlebarsin page420,5722.4e-31.29× slower1,258
Markoahead of time406,6562.5e-31.34× slower1,140
Nunjucksin page338,9493.0e-31.60× slower1,258
Teddy (tree walk)in page338,6343.0e-31.60× slower1,158
Dust.jsin page313,3673.2e-31.73× slower1,258
Lodash templatein page280,3373.6e-31.94× slower1,258
Mustachein page244,6724.1e-32.22× slower1,278
EJSin page145,4916.9e-33.74× slower1,258
LiquidJSin page13,4530.07440.40× 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,802,4631.7e-42.00× faster492
Etain page5,731,1351.7e-41.98× faster478
Squirrellyin page5,659,5701.8e-41.95× faster478
Pugahead of time5,361,5621.9e-41.85× faster410
doTin page4,747,0002.1e-41.64× faster492
Teddy (emitted js)ahead of time2,899,9543.4e-4same516
Nunjucksin page1,125,9608.9e-42.58× slower492
Dust.jsin page1,001,3261.0e-32.90× slower516
Markoahead of time733,9261.4e-33.95× slower392
Teddy (tree walk)in page681,3311.5e-34.26× slower410
Mustachein page608,5711.6e-34.77× slower454
Lodash templatein page550,3921.8e-35.27× slower492
Handlebarsin page477,8222.1e-36.07× slower453
EJSin page408,2522.4e-37.10× slower492
LiquidJSin page31,9160.03190.86× 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
art-templatein page276,6813.6e-31.36× faster11,627
Etain page266,1143.8e-31.31× faster11,425
Pugahead of time221,5724.5e-31.09× faster7,782
Teddy (emitted js)ahead of time203,9074.9e-3same11,795
Squirrellyin page188,4785.3e-31.08× slower11,425
Markoahead of time132,1367.6e-31.54× slower7,474
doTin page131,7277.6e-31.55× slower11,627
Dust.jsin page65,4630.0153.11× slower11,627
Handlebarsin page54,4120.0183.75× slower10,121
Mustachein page51,6810.0193.95× slower10,121
Lodash templatein page39,3290.0255.18× slower11,627
Nunjucksin page26,2310.0387.77× slower11,627
Teddy (tree walk)in page24,0450.0428.48× slower7,782
EJSin page19,3460.05210.54× slower11,627
LiquidJSin page1,3200.757154.42× 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
Etain page8,3290.1201.21× faster237,757
art-templatein page7,6110.1311.10× faster238,758
Pugahead of time6,9170.145same156,734
Teddy (emitted js)ahead of time6,8920.145same238,758
Squirrellyin page6,6390.1511.04× slower237,757
Markoahead of time6,5890.1521.05× slower156,732
doTin page4,0800.2451.69× slower238,758
Handlebarsin page3,0900.3242.23× slower233,753
Dust.jsin page2,5960.3852.66× slower238,758
Teddy (tree walk)in page2,0350.4913.39× slower156,734
Lodash templatein page1,8830.5313.66× slower238,758
Mustachein page1,8770.5333.67× slower233,753
Nunjucksin page1,7240.5804.00× slower238,758
EJSin page7621.319.05× slower238,758
LiquidJSin page50.519.79136.38× 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
Pugahead of time167,4106.0e-31.24× faster8,611
Teddy (emitted js)ahead of time134,9457.4e-3same10,958
Squirrellyin page126,4147.9e-31.07× slower10,672
Etain page119,5478.4e-31.13× slower10,672
Markoahead of time101,0339.9e-31.34× slower8,247
doTin page100,0651.0e-21.35× slower10,931
Dust.jsin page51,5750.0192.62× slower10,883
Handlebarsin page25,3420.0395.32× slower11,648
Mustachein page25,3060.0405.33× slower11,708
Lodash templatein page24,4440.0415.52× slower10,883
Teddy (tree walk)in page21,9340.0466.15× slower8,611
Nunjucksin page14,3890.0699.38× slower10,883
art-templatein page11,7680.08511.47× slower10,883
EJSin page9,0750.11014.87× slower10,883
LiquidJSin page6991.43193.17× 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
Pugahead of time112,0708.9e-31.25× faster11,824
Etain page95,4840.0101.06× faster17,067
Squirrellyin page93,3230.0111.04× faster17,067
Teddy (emitted js)ahead of time89,7850.011same17,397
Markoahead of time72,3080.0141.24× slower11,438
doTin page68,6150.0151.31× slower17,364
Dust.jsin page35,7670.0282.51× slower17,328
Handlebarsin page21,2850.0474.22× slower18,765
Lodash templatein page18,5350.0544.84× slower17,308
Mustachein page18,4600.0544.86× slower18,837
Teddy (tree walk)in page17,9050.0565.01× slower11,825
Nunjucksin page11,9980.0837.48× slower17,308
art-templatein page11,4860.0877.82× slower17,308
EJSin page7,2800.13712.33× slower17,308
LiquidJSin page5311.88169.01× 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
doT93,5630.01114.85× faster1,275
Eta34,4210.0295.46× faster1,258
Lodash template28,4420.0354.51× faster1,258
EJS26,5990.0384.22× faster1,258
Squirrelly23,4950.0433.73× faster1,258
Mustache13,6600.0732.17× faster1,278
art-template13,4630.0742.14× faster1,259
Nunjucks7,2420.1381.15× faster1,258
Teddy6,3000.159same1,158
LiquidJS5,2680.1901.20× slower1,258
Handlebars4,2370.2361.49× slower1,258
Dust.js3,3660.2971.87× 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
doT143,5287.0e-340.46× faster492
Eta42,4890.02411.98× faster478
Lodash template41,4540.02411.69× faster492
EJS39,4910.02511.13× faster492
Squirrelly26,4670.0387.46× faster478
art-template19,8250.0505.59× faster492
Mustache13,6360.0733.84× faster454
LiquidJS8,8440.1132.49× faster492
Nunjucks8,4210.1192.37× faster492
Handlebars4,2400.2361.20× faster453
Teddy3,5470.282same410
Dust.js3,4380.2911.03× slower516
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
doT77,5770.01314.96× faster11,627
Eta41,8560.0248.07× faster11,425
Squirrelly27,0820.0375.22× faster11,425
art-template25,8020.0394.97× faster11,627
Lodash template22,2000.0454.28× faster11,627
EJS14,7230.0682.84× faster11,627
Mustache14,6260.0682.82× faster10,121
Nunjucks7,9460.1261.53× faster11,627
Handlebars6,3680.1571.23× faster10,121
Teddy5,1870.193same7,782
Dust.js4,4160.2261.17× slower11,627
LiquidJS1,1190.8934.63× 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,2250.1384.77× faster237,757
art-template6,1200.1634.04× faster238,758
Squirrelly5,3740.1863.55× faster237,757
doT3,9350.2542.60× faster238,758
Handlebars2,1780.4591.44× faster233,753
Lodash template1,8140.5511.20× faster238,758
Dust.js1,7400.5751.15× faster238,758
Mustache1,6850.5931.11× faster233,753
Teddy1,5150.660same156,734
Nunjucks1,5090.663same238,758
EJS7521.332.01× slower238,758
LiquidJS49.420.2530.68× 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
doT49,0710.02016.69× faster10,931
Eta39,3550.02513.38× faster10,672
Squirrelly28,5030.0359.69× faster10,672
Lodash template17,4730.0575.94× faster10,883
Mustache8,8230.1133.00× faster11,708
EJS6,3680.1572.17× faster10,883
Dust.js5,5350.1811.88× faster10,883
art-template5,4960.1821.87× faster10,883
Nunjucks4,0240.2491.37× faster10,883
Teddy2,9400.340same8,611
Handlebars2,7790.3601.06× slower11,648
LiquidJS6491.544.53× 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
doT29,8820.03330.75× faster17,364
Eta24,8900.04025.61× faster17,067
Squirrelly18,0130.05618.54× faster17,067
Lodash template12,7020.07913.07× faster17,308
Mustache5,3830.1865.54× faster18,837
EJS4,6630.2144.80× faster17,308
art-template3,9320.2544.05× faster17,308
Dust.js2,8400.3522.92× faster17,328
Nunjucks2,4910.4012.56× faster17,308
Handlebars1,4970.6681.54× faster18,765
Teddy9721.03same11,825
LiquidJS4892.041.99× 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