Roosevelt

Teddy — Benchmarks

🔝 Scroll to top

Templating engine benchmarks

These benchmarks were ran on the following hardware:

Nodev24.19.0 (V8 13.6.233.17-node.51)
PlatformLinux 7.0.0-34-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.20× slower
Eta61.28× slower
Squirrelly61.38× slower
Marko61.94× slower
doT62.30× slower
art-template63.20× slower
Dust.js65.21× slower
Handlebars66.72× slower
Lodash template67.32× slower
Nunjucks69.28× slower
Mustache610.46× slower
EJS617.06× slower
PHP (node-php-runner)629.50× slower
LiquidJS683.10× slower

In Node, cold

Geometric mean of the per scenario ratios to Teddy.

EngineScenariosvs Teddy
doT613.44× faster
Eta69.43× faster
Squirrelly68.20× faster
Mustache63.60× faster
art-template63.21× faster
Lodash template62.97× faster
EJS62.67× faster
Dust.js61.57× faster
Nunjucks61.38× faster
Handlebars61.29× faster
Teddy6same
LiquidJS61.12× slower
PHP (node-php-runner)62.57× slower
Pug63.12× slower
Marko67.22× 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.30× faster
Eta61.23× faster
Squirrelly61.13× faster
Teddy (emitted js)6same
doT61.20× slower
Marko61.56× slower
art-template61.57× slower
Dust.js62.57× slower
Handlebars63.36× slower
Mustache64.00× slower
Lodash template64.19× slower
Teddy (tree walk)64.34× slower
Nunjucks64.64× slower
EJS68.89× slower
LiquidJS6119.73× slower
PHP (node-php-runner)0cannot run client-side

In a browser, cold

Geometric mean of the per scenario ratios to Teddy.

EngineScenariosvs Teddy
doT614.17× faster
Eta69.21× faster
Squirrelly66.15× faster
Lodash template65.28× faster
art-template63.54× faster
EJS63.01× faster
Mustache62.79× faster
Nunjucks61.59× faster
Dust.js61.19× faster
Handlebars61.16× faster
Teddy6same
LiquidJS62.88× 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,021,4411.0e-30.31.31× faster1,259
Pug3.0.4832,1261.2e-30.11.07× faster1,158
Teddy2.0.5776,7911.3e-30.4same1,258
Eta4.6.0613,9071.7e-30.41.27× slower1,258
Marko6.3.51530,4971.9e-30.21.46× slower1,140
Squirrelly9.1.1529,4991.9e-30.11.47× slower1,258
doT1.1.3429,0262.3e-30.11.81× slower1,275
Handlebars4.7.9338,3533.0e-30.22.30× slower1,258
Nunjucks3.2.4285,1523.5e-30.12.72× slower1,258
Dust.js3.0.1274,9403.7e-30.12.83× slower1,258
Lodash template4.18.1227,9574.4e-30.13.41× slower1,258
Mustache4.2.0132,4037.6e-30.15.87× slower1,278
EJS6.0.1121,8468.3e-30.16.38× slower1,258
PHP (node-php-runner)2.0.039,7980.0260.219.52× slower1,258
LiquidJS10.29.026,4470.0380.229.37× 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.19,561,1641.1e-41.61.37× faster478
Eta4.6.09,402,0761.1e-42.01.35× faster478
Pug3.0.49,091,6351.2e-42.61.31× faster410
art-template4.13.48,484,7801.2e-41.41.22× faster492
Teddy2.0.56,958,4581.5e-40.2same516
doT1.1.35,409,5931.9e-40.11.29× slower492
Marko6.3.511,686,7646.0e-40.14.13× slower392
Nunjucks3.2.41,187,6458.5e-40.15.86× slower492
Dust.js3.0.1959,2321.1e-30.47.25× slower516
Lodash template4.18.1522,5581.9e-30.113.32× slower492
Handlebars4.7.9423,9652.4e-30.216.41× slower453
EJS6.0.1372,9002.7e-30.118.66× slower492
Mustache4.2.0275,0783.7e-30.125.30× slower454
LiquidJS10.29.065,7520.0150.2105.83× slower492
PHP (node-php-runner)2.0.062,0020.0170.2112.23× 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.0.5292,7753.4e-30.1same11,795
Eta4.6.0227,1054.5e-30.21.29× slower11,425
art-template4.13.4226,9864.5e-30.21.29× slower11,627
Pug3.0.4200,9705.1e-30.31.46× slower7,782
Squirrelly9.1.1178,3665.7e-30.21.64× slower11,425
Marko6.3.51159,4726.3e-30.11.84× slower7,474
doT1.1.398,5510.0100.32.97× slower11,627
Dust.js3.0.145,0580.0220.16.50× slower11,627
Mustache4.2.041,1400.0240.27.12× slower10,121
Handlebars4.7.941,0700.0250.27.13× slower10,121
Lodash template4.18.136,6830.0270.17.98× slower11,627
EJS6.0.117,2180.0580.117.00× slower11,627
Nunjucks3.2.416,7140.0600.317.52× slower11,627
PHP (node-php-runner)2.0.012,0910.0850.424.21× slower11,425
LiquidJS10.29.02,4850.4040.3117.82× 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.0.59,0990.1130.6same238,758
art-template4.13.47,6280.1340.61.19× slower238,758
Eta4.6.07,4710.1360.41.22× slower237,757
Marko6.3.516,4390.1570.41.41× slower156,732
Squirrelly9.1.16,3040.1600.31.44× slower237,757
Pug3.0.46,1550.1650.51.48× slower156,734
doT1.1.33,1470.3230.72.89× slower238,758
Handlebars4.7.92,3480.4320.73.88× slower233,753
Dust.js3.0.11,8180.5540.55.01× slower238,758
Lodash template4.18.11,7000.5920.45.35× slower238,758
Mustache4.2.01,5010.6690.46.06× slower233,753
Nunjucks3.2.41,2840.7800.27.09× slower238,758
EJS6.0.16831.470.513.33× slower238,758
PHP (node-php-runner)2.0.05421.901.916.79× slower237,757
LiquidJS10.29.010010.051.890.97× 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.0.5208,1554.9e-30.2same10,958
Pug3.0.4149,6186.9e-30.21.39× slower8,611
Squirrelly9.1.1118,5998.6e-30.31.76× slower10,672
Eta4.6.0112,2369.1e-30.21.85× slower10,672
Marko6.3.51106,0559.6e-30.21.96× slower8,247
doT1.1.374,7360.0140.22.79× slower10,931
Dust.js3.0.137,3400.0270.35.57× slower10,883
Lodash template4.18.121,2580.0470.29.79× slower10,883
Handlebars4.7.919,6020.0520.310.62× slower11,648
Mustache4.2.013,5520.0750.315.36× slower11,708
Nunjucks3.2.410,2690.1134.520.27× slower10,883
PHP (node-php-runner)2.0.07,2300.1410.428.79× slower10,672
EJS6.0.16,0110.1680.334.63× slower10,883
art-template4.13.45,0310.2082.041.37× slower10,883
LiquidJS10.29.01,9990.5070.9104.15× 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.0.5137,0747.4e-30.2same17,397
Pug3.0.498,4970.0100.21.39× slower11,824
Squirrelly9.1.187,1360.0120.41.57× slower17,067
Eta4.6.086,9250.0120.41.58× slower17,067
Marko6.3.5179,1330.0130.41.73× slower11,438
doT1.1.352,0790.0190.22.63× slower17,364
Dust.js3.0.125,5860.0400.35.36× slower17,328
Lodash template4.18.116,8710.0600.28.12× slower17,308
Handlebars4.7.916,5010.0620.48.31× slower18,765
Mustache4.2.010,3020.0980.213.31× slower18,837
Nunjucks3.2.48,6160.1354.615.91× slower17,308
PHP (node-php-runner)2.0.05,3380.1900.425.68× slower17,067
EJS6.0.15,1850.1940.226.44× slower17,308
art-template4.13.45,0560.2061.727.11× slower17,308
LiquidJS10.29.01,4440.6990.694.92× 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.338,3100.0260.110.23× faster1,275
EJS6.0.117,4090.0580.24.65× faster1,258
Eta4.6.017,0950.0590.14.57× faster1,258
Squirrelly9.1.114,8250.0680.33.96× faster1,258
art-template4.13.410,9310.0930.62.92× faster1,259
Mustache4.2.09,9050.1020.22.65× faster1,278
Lodash template4.18.18,2370.1241.02.20× faster1,258
LiquidJS10.29.07,7360.1351.12.07× faster1,258
Nunjucks3.2.46,1120.1650.31.63× faster1,258
Teddy2.0.53,7440.2751.1same1,258
Handlebars4.7.92,6440.3850.71.42× slower1,258
Dust.js3.0.12,6320.3830.41.42× slower1,258
PHP (node-php-runner)2.0.06891.491.55.44× slower1,258
Pug3.0.44192.400.78.94× slower1,158
Marko6.3.513073.658.812.18× 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.346,9420.0220.242.81× faster492
EJS6.0.123,1030.0440.221.07× faster492
Eta4.6.018,9950.0530.217.32× faster478
Squirrelly9.1.116,1420.0620.214.72× faster478
art-template4.13.415,1630.0670.513.83× faster492
LiquidJS10.29.012,5390.0861.911.44× faster492
Mustache4.2.09,6690.1050.38.82× faster454
Lodash template4.18.18,8310.1161.08.05× faster492
Nunjucks3.2.47,4590.1350.36.80× faster492
Dust.js3.0.12,6200.3850.42.39× faster516
Handlebars4.7.92,5850.3930.72.36× faster453
Teddy2.0.51,0960.9351.3same516
Pug3.0.47061.430.71.55× slower410
PHP (node-php-runner)2.0.06871.491.51.60× slower478
Marko6.3.511875.745.65.88× 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,8470.0290.213.23× faster11,627
art-template4.13.420,5750.0490.47.81× faster11,627
Eta4.6.019,1560.0530.27.27× faster11,425
Squirrelly9.1.116,5680.0610.26.29× faster11,425
EJS6.0.111,7560.0850.24.46× faster11,627
Mustache4.2.010,4090.0970.23.95× faster10,121
Lodash template4.18.16,7260.1520.92.55× faster11,627
Nunjucks3.2.46,3720.1590.32.42× faster11,627
Handlebars4.7.93,8380.2680.91.46× faster10,121
Dust.js3.0.13,2700.3080.41.24× faster11,627
Teddy2.0.52,6330.3901.1same11,795
LiquidJS10.29.02,1560.4690.51.22× slower11,627
Pug3.0.47811.290.63.37× slower7,782
PHP (node-php-runner)2.0.06751.511.43.90× slower11,425
Marko6.3.513962.767.66.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
art-template4.13.45,7820.1770.72.19× faster238,758
Eta4.6.05,2260.1950.61.98× faster237,757
Squirrelly9.1.14,4810.2300.71.70× faster237,757
doT1.1.32,9500.3440.61.12× faster238,758
Teddy2.0.52,6360.3911.2same238,758
Handlebars4.7.91,5310.6630.81.72× slower233,753
Mustache4.2.01,3210.7650.72.00× slower233,753
Lodash template4.18.11,1471.1714.92.30× slower238,758
Nunjucks3.2.41,0920.9190.42.41× slower238,758
Dust.js3.0.11,0390.9730.72.54× slower238,758
Pug3.0.47721.310.83.42× slower156,734
EJS6.0.16761.480.23.90× slower238,758
Marko6.3.514842.183.75.45× slower156,732
PHP (node-php-runner)2.0.04252.401.76.20× slower237,757
LiquidJS10.29.097.110.361.927.15× 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.324,2250.0420.321.50× faster10,931
Eta4.6.019,1860.0530.217.03× faster10,672
Squirrelly9.1.117,6200.0570.215.64× faster10,672
Mustache4.2.05,5810.1810.34.95× faster11,708
Lodash template4.18.14,4060.2320.83.91× faster10,883
Dust.js3.0.13,7250.2720.53.31× faster10,883
Handlebars4.7.91,5900.6410.91.41× faster11,648
EJS6.0.11,2540.8000.31.11× faster10,883
Teddy2.0.51,1270.9151.5same10,958
art-template4.13.48781.161.11.28× slower10,883
LiquidJS10.29.05891.710.61.91× slower10,883
PHP (node-php-runner)2.0.05781.761.41.95× slower10,672
Nunjucks3.2.45381.881.12.09× slower10,883
Pug3.0.43602.811.33.13× slower8,611
Marko6.3.5110410.024.810.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,0600.0640.442.30× faster17,364
Eta4.6.013,7860.0740.336.31× faster17,067
Squirrelly9.1.111,8520.0860.331.21× faster17,067
Mustache4.2.03,6410.2790.59.59× faster18,837
Lodash template4.18.13,4030.2980.88.96× faster17,308
Dust.js3.0.12,0540.4920.55.41× faster17,328
EJS6.0.11,1050.9070.32.91× faster17,308
Handlebars4.7.99001.131.02.37× faster18,765
art-template4.13.47671.331.32.02× faster17,308
PHP (node-php-runner)2.0.05351.911.51.41× faster17,067
LiquidJS10.29.05191.930.61.37× faster17,308
Nunjucks3.2.44892.071.11.29× faster17,308
Teddy2.0.53802.691.7same17,397
Pug3.0.42054.900.61.86× slower11,824
Marko6.3.5175.814.2310.75.01× 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,1199.0e-42.08× faster1,259
Pugahead of time836,9301.2e-31.56× faster1,158
Etain page741,3761.3e-31.38× faster1,258
Squirrellyin page698,0651.4e-31.30× faster1,258
doTin page547,7041.8e-31.02× faster1,275
Teddy (emitted js)ahead of time536,6951.9e-3same1,258
Handlebarsin page416,0672.4e-31.29× slower1,258
Markoahead of time406,7912.5e-31.32× slower1,140
Nunjucksin page338,7053.0e-31.58× slower1,258
Teddy (tree walk)in page331,2393.0e-31.62× slower1,158
Dust.jsin page318,5673.1e-31.68× slower1,258
Lodash templatein page278,1533.6e-31.93× slower1,258
Mustachein page247,3564.0e-32.17× slower1,278
EJSin page143,0657.0e-33.75× slower1,258
LiquidJSin page13,9020.07238.61× 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,752,1111.7e-41.81× faster492
Etain page5,638,2841.8e-41.77× faster478
Squirrellyin page5,607,2901.8e-41.76× faster478
Pugahead of time5,514,3421.8e-41.73× faster410
doTin page4,788,8372.1e-41.51× faster492
Teddy (emitted js)ahead of time3,180,7083.1e-4same516
Nunjucksin page1,123,7248.9e-42.83× slower492
Dust.jsin page1,010,5629.9e-43.15× slower516
Markoahead of time701,5611.4e-34.53× slower392
Teddy (tree walk)in page691,9811.4e-34.60× slower410
Mustachein page609,8541.6e-35.22× slower454
Handlebarsin page536,1281.9e-35.93× slower453
Lodash templatein page533,2661.9e-35.96× slower492
EJSin page402,2132.5e-37.91× slower492
LiquidJSin page29,1600.034109.08× 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 page272,5483.7e-31.32× faster11,627
Etain page261,6513.8e-31.27× faster11,425
Pugahead of time222,9934.5e-31.08× faster7,782
Teddy (emitted js)ahead of time205,8704.9e-3same11,795
Squirrellyin page184,8675.4e-31.11× slower11,425
Markoahead of time126,7117.9e-31.62× slower7,474
doTin page124,9258.0e-31.65× slower11,627
Dust.jsin page65,2340.0153.16× slower11,627
Handlebarsin page53,3240.0193.86× slower10,121
Mustachein page50,7370.0204.06× slower10,121
Lodash templatein page39,4550.0255.22× slower11,627
Nunjucksin page25,4780.0398.08× slower11,627
Teddy (tree walk)in page23,1470.0438.89× slower7,782
EJSin page19,0940.05210.78× slower11,627
LiquidJSin page1,2820.780160.60× 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,0630.1241.19× faster237,757
art-templatein page7,4380.1341.10× faster238,758
Pugahead of time6,9700.1431.03× faster156,734
Teddy (emitted js)ahead of time6,7630.148same238,758
Markoahead of time6,6620.1501.02× slower156,732
Squirrellyin page6,5010.1541.04× slower237,757
doTin page3,9490.2531.71× slower238,758
Handlebarsin page2,9650.3372.28× slower233,753
Dust.jsin page2,5480.3922.65× slower238,758
Teddy (tree walk)in page1,9690.5083.43× slower156,734
Mustachein page1,8670.5363.62× slower233,753
Lodash templatein page1,8450.5423.66× slower238,758
Nunjucksin page1,6210.6174.17× slower238,758
EJSin page7501.339.01× slower238,758
LiquidJSin page48.120.81140.74× 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 time165,7066.0e-31.31× faster8,611
Squirrellyin page127,0847.9e-3same10,672
Teddy (emitted js)ahead of time126,7187.9e-3same10,958
Etain page111,1849.0e-31.14× slower10,672
Markoahead of time105,5899.5e-31.20× slower8,247
doTin page100,8789.9e-31.26× slower10,931
Dust.jsin page52,0780.0192.43× slower10,883
Lodash templatein page24,6860.0415.13× slower10,883
Handlebarsin page24,6690.0415.14× slower11,648
Mustachein page24,3640.0415.20× slower11,708
Teddy (tree walk)in page21,6690.0465.85× slower8,611
Nunjucksin page14,4530.0698.77× slower10,883
art-templatein page11,9880.08310.57× slower10,883
EJSin page9,0480.11114.00× slower10,883
LiquidJSin page6941.44182.59× 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 time110,4879.1e-31.24× faster11,824
Etain page94,2510.0111.05× faster17,067
Squirrellyin page92,3070.0111.03× faster17,067
Teddy (emitted js)ahead of time89,4290.011same17,397
Markoahead of time73,5850.0141.22× slower11,438
doTin page69,5060.0141.29× slower17,364
Dust.jsin page33,4460.0302.67× slower17,328
Handlebarsin page21,4120.0474.18× slower18,765
Mustachein page18,8010.0534.76× slower18,837
Lodash templatein page18,7070.0534.78× slower17,308
Teddy (tree walk)in page17,7510.0565.04× slower11,825
Nunjucksin page11,9440.0847.49× slower17,308
art-templatein page11,5780.0867.72× slower17,308
EJSin page7,3240.13712.21× slower17,308
LiquidJSin page5281.90169.49× 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,3580.01115.01× faster1,275
Eta34,7790.0295.59× faster1,258
Lodash template27,9650.0364.50× faster1,258
EJS27,2110.0374.38× faster1,258
Squirrelly22,9840.0443.70× faster1,258
Mustache13,9120.0722.24× faster1,278
art-template13,6480.0732.19× faster1,259
Nunjucks7,3550.1361.18× faster1,258
Teddy6,2180.161same1,158
LiquidJS5,3140.1881.17× slower1,258
Handlebars4,3240.2311.44× slower1,258
Dust.js3,3570.2981.85× 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
doT141,2717.1e-340.33× faster492
Lodash template41,2760.02411.78× faster492
EJS38,7080.02611.05× faster492
Eta27,5770.0367.87× faster478
Squirrelly20,6760.0485.90× faster478
art-template19,5570.0515.58× faster492
Mustache13,8090.0723.94× faster454
LiquidJS8,4680.1182.42× faster492
Nunjucks8,2310.1212.35× faster492
Handlebars4,2710.2341.22× faster453
Teddy3,5030.285same410
Dust.js3,4040.2941.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,0420.01314.97× faster11,627
Eta41,5740.0248.08× faster11,425
Squirrelly26,3970.0385.13× faster11,425
art-template25,9290.0395.04× faster11,627
Lodash template22,4160.0454.36× faster11,627
EJS14,7980.0682.88× faster11,627
Mustache14,2060.0702.76× faster10,121
Nunjucks8,0660.1241.57× faster11,627
Handlebars6,6450.1501.29× faster10,121
Teddy5,1450.194same7,782
Dust.js4,3770.2281.18× slower11,627
LiquidJS1,1500.8694.47× 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,1450.1404.85× faster237,757
art-template6,2990.1594.28× faster238,758
Squirrelly5,4180.1853.68× faster237,757
doT3,8730.2582.63× faster238,758
Handlebars2,2310.4481.51× faster233,753
Lodash template1,7980.5561.22× faster238,758
Mustache1,7200.5811.17× faster233,753
Dust.js1,7000.5881.15× faster238,758
Nunjucks1,5200.6581.03× faster238,758
Teddy1,4730.679same156,734
EJS7541.331.96× slower238,758
LiquidJS48.720.5130.23× 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
Eta39,3270.02513.03× faster10,672
doT33,7260.03011.17× faster10,931
Squirrelly20,3340.0496.74× faster10,672
Lodash template17,1070.0585.67× faster10,883
Mustache8,6320.1162.86× faster11,708
EJS6,2010.1612.05× faster10,883
Dust.js5,4910.1821.82× faster10,883
art-template5,3940.1851.79× faster10,883
Nunjucks4,0030.2501.33× faster10,883
Teddy3,0190.331same8,611
Handlebars2,8240.3541.07× slower11,648
LiquidJS6601.524.58× 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
doT27,8890.03630.39× faster17,364
Eta24,8940.04027.13× faster17,067
Squirrelly17,9760.05619.59× faster17,067
Lodash template12,3960.08113.51× faster17,308
Mustache5,3040.1895.78× faster18,837
EJS4,6520.2155.07× faster17,308
art-template3,8450.2604.19× faster17,308
Dust.js2,8470.3513.10× faster17,328
Nunjucks2,4730.4042.69× faster17,308
Handlebars1,4770.6771.61× faster18,765
Teddy9181.09same11,825
LiquidJS4782.091.92× 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