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-42-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.24× slower
Eta61.29× slower
Squirrelly61.35× slower
Marko61.95× slower
doT62.64× slower
art-template63.01× slower
Dust.js65.77× slower
Handlebars66.33× slower
Lodash template67.39× slower
Mustache68.99× slower
Nunjucks69.51× slower
EJS617.30× slower
PHP (node-php-runner)630.81× slower
LiquidJS683.08× slower

In Node, cold

Geometric mean of the per scenario ratios to Teddy.

EngineScenariosvs Teddy
doT616.04× faster
Eta613.10× faster
Squirrelly69.89× faster
art-template63.96× faster
Mustache63.85× faster
Lodash template63.53× faster
EJS63.23× faster
Handlebars61.48× faster
Nunjucks61.48× faster
Dust.js61.39× faster
Teddy6same
LiquidJS6same
PHP (node-php-runner)61.74× slower
Pug62.86× slower
Marko66.43× 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.15× slower
Eta61.21× slower
Squirrelly61.30× slower
doT61.72× slower
art-template62.34× slower
Marko62.36× slower
Dust.js64.10× slower
Handlebars65.26× slower
Mustache65.89× slower
Teddy (tree walk)66.15× slower
Lodash template66.23× slower
Nunjucks66.78× slower
EJS613.18× slower
LiquidJS6180.39× slower
PHP (node-php-runner)0cannot run client-side

In a browser, cold

Geometric mean of the per scenario ratios to Teddy.

EngineScenariosvs Teddy
doT617.29× faster
Eta611.13× faster
Squirrelly68.00× faster
Lodash template66.05× faster
art-template63.88× faster
EJS63.48× faster
Mustache63.17× faster
Nunjucks61.79× faster
Dust.js61.32× faster
Handlebars61.31× faster
Teddy6same
LiquidJS62.57× 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,0188.7e-40.81.61× faster1,259
Teddy2.3.2745,8301.4e-30.4same1,258
Eta4.6.0639,9961.6e-30.21.17× slower1,258
Pug3.0.4623,0501.6e-30.21.20× slower1,158
Squirrelly9.1.1574,2161.8e-30.31.30× slower1,258
Marko6.3.51529,3231.9e-30.21.41× slower1,140
doT1.1.3400,2622.5e-30.11.86× slower1,275
Handlebars4.7.9343,0312.9e-30.22.17× slower1,258
Nunjucks3.2.4293,4533.4e-30.12.54× slower1,258
Dust.js3.0.1239,3354.2e-30.23.12× slower1,258
Lodash template4.18.1237,7784.2e-30.13.14× slower1,258
Mustache4.2.0162,9916.3e-30.24.58× slower1,278
EJS6.0.1124,2228.1e-30.76.00× slower1,258
PHP (node-php-runner)2.0.035,9500.0300.520.75× slower1,258
LiquidJS10.29.026,7370.0380.227.90× 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,409,5481.0e-41.71.63× faster478
Eta4.6.09,993,8821.1e-42.61.57× faster478
art-template4.13.49,731,4211.1e-41.71.53× faster492
Pug3.0.49,482,9171.2e-40.51.49× faster410
Teddy2.3.26,373,4061.6e-40.3same516
doT1.1.35,108,7612.0e-40.61.25× slower492
Marko6.3.511,687,6286.2e-40.43.78× slower392
Nunjucks3.2.41,172,8238.8e-40.25.43× slower492
Dust.js3.0.1922,6591.1e-31.46.91× slower516
Lodash template4.18.1529,4861.9e-30.112.04× slower492
Mustache4.2.0516,0332.0e-30.212.35× slower454
Handlebars4.7.9467,9312.2e-30.313.62× slower453
EJS6.0.1369,3372.8e-30.317.26× slower492
LiquidJS10.29.065,9380.0150.296.66× slower492
PHP (node-php-runner)2.0.065,4080.0160.497.44× 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.3.2304,5063.4e-30.2same11,795
art-template4.13.4240,9154.3e-30.31.26× slower11,627
Eta4.6.0229,6574.5e-30.31.33× slower11,425
Pug3.0.4209,2454.9e-30.21.46× slower7,782
Squirrelly9.1.1180,4515.7e-30.31.69× slower11,425
Marko6.3.51157,0616.5e-30.31.94× slower7,474
doT1.1.382,8590.0120.43.67× slower11,627
Handlebars4.7.945,2110.0230.46.74× slower10,121
Dust.js3.0.140,4190.0250.37.53× slower11,627
Mustache4.2.037,9220.0270.38.03× slower10,121
Lodash template4.18.137,0870.0270.28.21× slower11,627
EJS6.0.117,6330.0570.117.27× slower11,627
Nunjucks3.2.417,1800.0590.217.72× slower11,627
PHP (node-php-runner)2.0.011,8400.0860.325.72× slower11,425
LiquidJS10.29.02,6790.3750.4113.65× 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.3.29,8280.1070.8same238,758
Eta4.6.07,8600.1290.41.25× slower237,757
art-template4.13.47,3460.1400.61.34× slower238,758
Marko6.3.516,6230.1530.41.48× slower156,732
Pug3.0.46,5100.1590.71.51× slower156,734
Squirrelly9.1.16,3160.1610.51.56× slower237,757
doT1.1.32,7450.3680.53.58× slower238,758
Handlebars4.7.92,7020.3810.93.64× slower233,753
Dust.js3.0.11,7280.5850.65.69× slower238,758
Lodash template4.18.11,7000.5930.65.78× slower238,758
Mustache4.2.01,3230.7620.67.43× slower233,753
Nunjucks3.2.41,1500.8730.48.55× slower238,758
EJS6.0.16511.550.815.09× slower238,758
PHP (node-php-runner)2.0.04952.071.619.86× slower237,757
LiquidJS10.29.010010.010.998.24× 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.3.2218,6814.8e-30.3same10,958
Pug3.0.4151,2226.9e-30.31.45× slower8,611
Squirrelly9.1.1125,8158.1e-30.31.74× slower10,672
Marko6.3.51112,8039.0e-30.21.94× slower8,247
Eta4.6.0108,7649.5e-30.32.01× slower10,672
doT1.1.366,1810.0150.33.30× slower10,931
Dust.js3.0.134,6620.0290.26.31× slower10,883
Lodash template4.18.121,5060.0470.410.17× slower10,883
Handlebars4.7.920,9550.0490.310.44× slower11,648
Mustache4.2.016,8080.0600.213.01× slower11,708
Nunjucks3.2.49,8900.1203.822.11× slower10,883
PHP (node-php-runner)2.0.06,8390.1530.731.97× slower10,672
EJS6.0.16,2110.1620.235.21× slower10,883
art-template4.13.45,5320.1931.939.53× slower10,883
LiquidJS10.29.02,0420.4960.6107.09× 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.3.2150,0706.8e-30.3same17,397
Pug3.0.4103,8889.8e-30.31.44× slower11,824
Squirrelly9.1.188,4370.0120.51.70× slower17,067
Eta4.6.082,5670.0130.51.82× slower17,067
Marko6.3.5182,0890.0120.31.83× slower11,438
doT1.1.344,4910.0230.63.37× slower17,364
Dust.js3.0.123,6320.0430.46.35× slower17,328
Handlebars4.7.917,6640.0570.38.50× slower18,765
Lodash template4.18.116,7930.0600.48.94× slower17,308
Mustache4.2.012,4540.0810.212.05× slower18,837
Nunjucks3.2.49,3570.1293.916.04× slower17,308
PHP (node-php-runner)2.0.05,7890.1780.625.92× slower17,067
art-template4.13.45,5060.1951.827.26× slower17,308
EJS6.0.15,3160.1890.228.23× slower17,308
LiquidJS10.29.01,4710.6840.5102.00× 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,9620.0250.214.47× faster1,275
Eta4.6.020,1380.0510.47.11× faster1,258
EJS6.0.117,1690.0610.56.07× faster1,258
Squirrelly9.1.115,3360.0660.35.42× faster1,258
art-template4.13.411,6730.0890.84.12× faster1,259
Mustache4.2.09,0580.1130.43.20× faster1,278
Lodash template4.18.18,3050.1271.32.93× faster1,258
LiquidJS10.29.06,6810.1611.52.36× faster1,258
Nunjucks3.2.46,6630.1520.42.35× faster1,258
Teddy2.3.22,8300.3741.4same1,258
Handlebars4.7.92,4110.4240.81.17× slower1,258
Dust.js3.0.11,7560.5790.81.61× slower1,258
PHP (node-php-runner)2.0.07891.311.43.59× slower1,258
Pug3.0.43752.691.17.55× slower1,158
Marko6.3.512783.915.310.17× 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,4710.0200.253.54× faster492
EJS6.0.124,8760.0410.325.88× faster492
Eta4.6.023,2150.0430.224.15× faster478
Squirrelly9.1.116,5000.0610.317.16× faster478
art-template4.13.415,7270.0671.016.36× faster492
LiquidJS10.29.012,0480.0901.712.53× faster492
Lodash template4.18.19,3300.1142.19.71× faster492
Mustache4.2.08,4380.1200.58.78× faster454
Nunjucks3.2.45,7770.1750.36.01× faster492
Handlebars4.7.92,3350.4390.82.43× faster453
Dust.js3.0.11,8880.5310.31.96× faster516
PHP (node-php-runner)2.0.01,1681.051.41.22× faster478
Teddy2.3.29611.071.4same516
Pug3.0.46961.450.91.38× slower410
Marko6.3.511835.734.25.24× 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.333,8040.0310.615.64× faster11,627
Eta4.6.023,0890.0450.410.68× faster11,425
art-template4.13.421,4090.0490.89.91× faster11,627
Squirrelly9.1.116,4520.0630.57.61× faster11,425
EJS6.0.111,9290.0840.25.52× faster11,627
Mustache4.2.08,9140.1130.34.12× faster10,121
Lodash template4.18.16,5770.1611.33.04× faster11,627
Nunjucks3.2.45,5060.1840.42.55× faster11,627
Handlebars4.7.93,6090.2880.91.67× faster10,121
Dust.js3.0.12,4380.4180.71.13× faster11,627
Teddy2.3.22,1610.4851.4same11,795
LiquidJS10.29.02,1300.4810.81.01× slower11,627
PHP (node-php-runner)2.0.08821.161.12.45× slower11,425
Pug3.0.47101.420.83.04× slower7,782
Marko6.3.513892.723.45.56× 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,8550.1750.62.43× faster237,757
art-template4.13.45,8150.1770.72.42× faster238,758
Squirrelly9.1.14,6210.2230.71.92× faster237,757
doT1.1.32,5650.3970.71.07× faster238,758
Teddy2.3.22,4050.4311.2same238,758
Handlebars4.7.91,7690.5750.81.36× slower233,753
Mustache4.2.01,1620.8660.62.07× slower233,753
Lodash template4.18.11,0861.3714.32.22× slower238,758
Nunjucks3.2.41,0080.9970.52.39× slower238,758
Dust.js3.0.19541.050.62.52× slower238,758
Pug3.0.46941.461.13.47× slower156,734
EJS6.0.16441.560.73.73× slower238,758
Marko6.3.514672.313.75.15× slower156,732
PHP (node-php-runner)2.0.04392.331.65.47× slower237,757
LiquidJS10.29.095.210.551.425.25× 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,3270.0400.225.77× faster10,931
Eta4.6.022,2170.0470.422.60× faster10,672
Squirrelly9.1.117,6210.0580.717.93× faster10,672
Mustache4.2.05,2540.1940.65.35× faster11,708
Lodash template4.18.14,5730.2251.14.65× faster10,883
Dust.js3.0.12,8700.3510.42.92× faster10,883
Handlebars4.7.91,5600.6540.91.59× faster11,648
EJS6.0.11,3070.7680.41.33× faster10,883
Teddy2.3.29831.061.7same10,958
art-template4.13.49371.091.11.05× slower10,883
PHP (node-php-runner)2.0.07431.360.81.32× slower10,672
LiquidJS10.29.05731.750.51.71× slower10,883
Nunjucks3.2.44422.291.32.22× slower10,883
Pug3.0.43632.760.72.70× slower8,611
Marko6.3.5110210.134.69.60× 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,2310.0630.451.07× faster17,364
Eta4.6.015,9270.0640.450.11× faster17,067
Squirrelly9.1.112,2050.0840.438.40× faster17,067
Mustache4.2.03,4720.2930.610.92× faster18,837
Lodash template4.18.13,3800.3051.210.64× faster17,308
Dust.js3.0.11,4340.7090.94.51× faster17,328
EJS6.0.11,1660.8640.63.67× faster17,308
Handlebars4.7.98401.211.02.64× faster18,765
art-template4.13.47961.301.72.51× faster17,308
PHP (node-php-runner)2.0.06041.731.91.90× faster17,067
Nunjucks3.2.44942.061.31.55× faster17,308
LiquidJS10.29.04612.191.01.45× faster17,308
Teddy2.3.23183.222.0same17,397
Pug3.0.41745.801.41.83× slower11,824
Marko6.3.5165.815.725.04.83× 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,118,5528.9e-41.40× faster1,259
Pugahead of time838,0801.2e-31.05× faster1,158
Teddy (emitted js)ahead of time800,3711.2e-3same1,258
Etain page723,8911.4e-31.11× slower1,258
Squirrellyin page686,7011.5e-31.17× slower1,258
doTin page536,1011.9e-31.49× slower1,275
Markoahead of time402,5582.5e-31.99× slower1,140
Teddy (tree walk)in page394,5682.5e-32.03× slower1,258
Handlebarsin page382,7182.6e-32.09× slower1,258
Nunjucksin page325,0643.1e-32.46× slower1,258
Dust.jsin page293,6273.4e-32.73× slower1,258
Lodash templatein page284,9763.5e-32.81× slower1,258
Mustachein page239,9454.2e-33.34× slower1,278
EJSin page141,5297.1e-35.66× slower1,258
LiquidJSin page13,4160.07559.66× 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
Etain page5,691,6451.8e-41.69× faster478
art-templatein page5,672,8601.8e-41.68× faster492
Squirrellyin page5,382,7951.9e-41.59× faster478
Pugahead of time4,756,9462.1e-41.41× faster410
doTin page4,683,5802.1e-41.39× faster492
Teddy (emitted js)ahead of time3,377,7153.0e-4same516
Nunjucksin page1,138,5338.8e-42.97× slower492
Dust.jsin page1,002,2991.0e-33.37× slower516
Teddy (tree walk)in page693,0141.4e-34.87× slower516
Mustachein page605,3891.7e-35.58× slower454
Markoahead of time587,6691.7e-35.75× slower392
Lodash templatein page555,0571.8e-36.09× slower492
Handlebarsin page525,8291.9e-36.42× slower453
EJSin page400,9792.5e-38.42× slower492
LiquidJSin page30,5730.033110.48× 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 time323,7933.1e-3same11,795
art-templatein page239,0524.2e-31.35× slower11,627
Pugahead of time217,7774.6e-31.49× slower7,782
Etain page215,6954.6e-31.50× slower11,425
Squirrellyin page187,7735.3e-31.72× slower11,425
Markoahead of time130,2427.7e-32.49× slower7,474
doTin page127,2297.9e-32.54× slower11,627
Dust.jsin page63,5200.0165.10× slower11,627
Handlebarsin page54,2090.0185.97× slower10,121
Mustachein page50,4780.0206.41× slower10,121
Lodash templatein page31,0710.03210.42× slower11,627
Nunjucksin page24,9750.04012.96× slower11,627
Teddy (tree walk)in page23,2170.04313.95× slower11,795
EJSin page19,1510.05216.91× slower11,627
LiquidJSin page1,1880.842272.63× 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 time9,7230.103same238,758
Etain page8,1390.1231.19× slower237,757
art-templatein page7,4160.1351.31× slower238,758
Pugahead of time6,9440.1441.40× slower156,734
Squirrellyin page6,5270.1531.49× slower237,757
Markoahead of time6,3690.1571.53× slower156,732
doTin page4,1250.2422.36× slower238,758
Handlebarsin page2,1160.4734.60× slower233,753
Dust.jsin page1,9720.5074.93× slower238,758
Lodash templatein page1,8450.5425.27× slower238,758
Mustachein page1,7150.5835.67× slower233,753
Teddy (tree walk)in page1,6390.6105.93× slower238,758
Nunjucksin page1,5870.6306.13× slower238,758
EJSin page6711.4914.49× slower238,758
LiquidJSin page43.522.99223.54× 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 time180,8395.5e-3same10,958
Pugahead of time159,2676.3e-31.14× slower8,611
Squirrellyin page125,3768.0e-31.44× slower10,672
Etain page113,8178.8e-31.59× slower10,672
doTin page101,5609.8e-31.78× slower10,931
Markoahead of time96,4790.0101.87× slower8,247
Dust.jsin page51,4550.0193.51× slower10,883
Mustachein page24,9390.0407.25× slower11,708
Handlebarsin page24,5620.0417.36× slower11,648
Lodash templatein page24,3230.0417.43× slower10,883
Teddy (tree walk)in page23,1880.0437.80× slower10,958
Nunjucksin page14,0410.07112.88× slower10,883
art-templatein page11,5860.08615.61× slower10,883
EJSin page8,7510.11420.66× slower10,883
LiquidJSin page6561.52275.52× 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 time157,7306.3e-3same17,397
Pugahead of time112,0048.9e-31.41× slower11,824
Etain page93,3210.0111.69× slower17,067
Squirrellyin page90,4960.0111.74× slower17,067
Markoahead of time74,4360.0132.12× slower11,438
doTin page70,0500.0142.25× slower17,364
Dust.jsin page27,0030.0375.84× slower17,328
Handlebarsin page20,3000.0497.77× slower18,765
Lodash templatein page18,7930.0538.39× slower17,308
Teddy (tree walk)in page18,5710.0548.49× slower17,397
Mustachein page18,4860.0548.53× slower18,837
Nunjucksin page12,1500.08212.98× slower17,308
art-templatein page11,4090.08813.83× slower17,308
EJSin page7,2590.13821.73× slower17,308
LiquidJSin page5071.97311.32× 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
doT89,1950.01116.53× faster1,275
Eta32,8920.0306.10× faster1,258
Lodash template27,7020.0365.14× faster1,258
EJS26,4620.0384.91× faster1,258
Squirrelly23,2760.0434.31× faster1,258
Mustache13,4570.0742.49× faster1,278
art-template13,1730.0762.44× faster1,259
Nunjucks7,3540.1361.36× faster1,258
Teddy5,3940.185same1,258
LiquidJS5,0200.1991.07× slower1,258
Handlebars4,0170.2491.34× slower1,258
Dust.js3,2310.3091.67× 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
doT120,4068.3e-342.95× faster492
Lodash template41,2270.02414.70× faster492
Eta40,1680.02514.33× faster478
EJS37,7860.02613.48× faster492
Squirrelly24,6160.0418.78× faster478
art-template13,6500.0734.87× faster492
Mustache12,6690.0794.52× faster454
LiquidJS7,8270.1282.79× faster492
Nunjucks7,7910.1282.78× faster492
Handlebars3,8990.2561.39× faster453
Dust.js3,1400.3191.12× faster516
Teddy2,8040.357same516
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
doT75,3250.01321.93× faster11,627
Eta38,3340.02611.16× faster11,425
Squirrelly25,8460.0397.52× faster11,425
art-template25,4660.0397.41× faster11,627
Lodash template21,4340.0476.24× faster11,627
EJS12,7330.0793.71× faster11,627
Mustache11,9680.0843.48× faster10,121
Nunjucks6,3050.1591.84× faster11,627
Handlebars5,5350.1811.61× faster10,121
Dust.js3,5950.2781.05× faster11,627
Teddy3,4350.291same11,795
LiquidJS1,0610.9423.24× 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
Eta6,8140.1474.38× faster237,757
art-template5,8380.1713.75× faster238,758
Squirrelly5,2900.1893.40× faster237,757
doT3,6740.2722.36× faster238,758
Handlebars2,1570.4641.39× faster233,753
Lodash template1,7870.5601.15× faster238,758
Mustache1,6780.5961.08× faster233,753
Dust.js1,6570.6031.07× faster238,758
Teddy1,5550.643same238,758
Nunjucks1,4770.6771.05× slower238,758
EJS7281.372.14× slower238,758
LiquidJS47.321.1432.87× 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
doT46,5720.02119.44× faster10,931
Eta31,9420.03113.33× faster10,672
Squirrelly27,3670.03711.42× faster10,672
Lodash template13,1920.0765.51× faster10,883
Mustache8,3030.1203.47× faster11,708
EJS6,2400.1602.60× faster10,883
art-template4,7830.2092.00× faster10,883
Dust.js4,7320.2111.98× faster10,883
Nunjucks3,7980.2631.59× faster10,883
Handlebars2,6810.3731.12× faster11,648
Teddy2,3960.417same10,958
LiquidJS5451.834.40× 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,0690.03637.30× faster17,364
Eta25,1480.04033.42× faster17,067
Squirrelly17,7890.05623.64× faster17,067
Lodash template12,3800.08116.45× faster17,308
Mustache5,1590.1946.86× faster18,837
EJS4,4820.2235.96× faster17,308
art-template3,8830.2585.16× faster17,308
Dust.js2,7380.3653.64× faster17,328
Nunjucks2,3760.4213.16× faster17,308
Handlebars1,4590.6851.94× faster18,765
Teddy7521.33same17,397
LiquidJS4702.131.60× 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