MessageFormat parse and render benchmark
In this benchmark we’ll look at two metrics : Library size and parsing/rendering speed.
Like any synthetic benchmark these should be taken with a pinch of salt. I tried to compare apples with apples as much as possible, but some libraries do a bit more and some others do a bit less.
Feel free to make a PR to help make these comparisons as fair as possible.
As a final word, I made this comparison purely for fun and to learn a thing or two about performance optimization.
Libraries size
Sources can be found in src
, measure taken on 07/12/2023 with latest available versions
Npm Package | Version | Size | Comment |
---|---|---|---|
@ffz/icu-msgparser (+ custom renderer) | 2.0.0 | 9.4K | |
@onigoetz/messageformat (+ @onigoetz/intl-formatters) | 1.0.0-rc.2 | 8K | |
@onigoetz/messageformat (+ @onigoetz/make-plural) | 1.0.0-rc.2 | 11K | |
format-message-parse | 6.2.4 | 22K | Uses peg.js |
@onigoetz/messageformat (+ make-plural) | 1.0.0-rc.2 | 23K | |
@onigoetz/messageformat (+ @phensley/plurals) | 1.0.0-rc.2 | 40K | |
intl-messageformat | 10.5.14 | 55K | Uses peg.js |
@phensley/messageformat | 1.9.0 | 54K | |
@messageformat/core | 3.4.0 | 74K | Uses peg.js |
globalize | 1.7.0 | 158K | Uses peg.js |
Notes:
-
globalize
comes with a bundled version ofmessageformat
0.3.0 andmake-plural
3.0.0. Also, it requires ICU data to know how to format numbers, currencies, timezones and more. It also comes with a compiler to remove the parts that aren’t required, but for my use case I consider that we can’t know in advance what is going to be formatted. -
@ffz/icu-msgparser
is only a parser, I added a renderer to it but did not add any number/date formatter (hence the comparatively small size). -
@eo-locale/core
was excluded from this list as it crashes on our valid test strings. It would be a strong contender as it has a very small footprint (4.6KB).
Benchmark
To make the benchmark compareable I tried to apply the same rules to all libraries. Each libraries must follow the same rules:
- Parse and format a string.
- Properly handles plurals.
- Do not perform any number or date formatting since not all libraries support them and it would give a serious boost to those.
- Give the same output as all other implementations.
- Compiled with the same tools and options.
- Use an identical method signature for all libraries.
The benchmark is applied to 4 different strings, which for the simple cases should be fairly common in applications and more advanced case are probably not as common but should still be performant.
Benchmarks run on
- Node.js v20.9.0
- Apple M2 CPU
- October 19, 2024
Simple String
const input = [`Hello, world!`, {}];
// Renders: `Hello, world!`
Name | ops/sec | MoE | Runs sampled |
---|---|---|---|
@onigoetz/messageformat (+ @onigoetz/make-plural) | 19,206,804 | ± 0.10% | 102 |
@onigoetz/messageformat (+ @phensley/plurals) | 19,192,893 | ± 0.08% | 99 |
@onigoetz/messageformat (+ @onigoetz/intl-formatters) | 19,095,730 | ± 0.35% | 97 |
@onigoetz/messageformat (+ make-plural) | 18,479,888 | ± 0.13% | 102 |
format-message-parse | 8,778,900 | ± 0.18% | 97 |
@phensley/messageformat | 8,086,805 | ± 0.19% | 99 |
@ffz/icu-msgparser (+ custom renderer) | 5,719,901 | ± 0.36% | 101 |
@messageformat/core | 1,724,418 | ± 0.12% | 95 |
intl-messageformat | 244,193 | ± 0.54% | 96 |
globalize | 37,342 | ± 0.31% | 97 |
With one variable
const input = [
`Hello, {name}!`,
{
name: "John",
},
];
// Renders: `Hello, John!`
Name | ops/sec | MoE | Runs sampled |
---|---|---|---|
@onigoetz/messageformat (+ make-plural) | 7,485,310 | ± 0.28% | 97 |
@onigoetz/messageformat (+ @onigoetz/intl-formatters) | 7,365,456 | ± 0.18% | 100 |
@onigoetz/messageformat (+ @onigoetz/make-plural) | 6,748,444 | ± 0.45% | 97 |
@onigoetz/messageformat (+ @phensley/plurals) | 6,450,655 | ± 0.78% | 100 |
format-message-parse | 3,966,869 | ± 0.26% | 98 |
@phensley/messageformat | 3,368,607 | ± 0.10% | 99 |
@ffz/icu-msgparser (+ custom renderer) | 3,346,844 | ± 0.13% | 101 |
@messageformat/core | 843,570 | ± 0.28% | 101 |
intl-messageformat | 213,755 | ± 0.63% | 92 |
globalize | 36,313 | ± 0.16% | 98 |
With plurals
const input = [
`Yo, {firstName} {lastName} has {numBooks} {numBooks, plural, one {book} other {books}}.`,
{
firstName: "John",
lastName: "Constantine",
numBooks: 5,
},
];
// Renders: `Yo, John Constantine has 5 books.`
Name | ops/sec | MoE | Runs sampled |
---|---|---|---|
@onigoetz/messageformat (+ @phensley/plurals) | 987,936 | ± 0.16% | 98 |
@onigoetz/messageformat (+ make-plural) | 969,817 | ± 0.09% | 100 |
@onigoetz/messageformat (+ @onigoetz/intl-formatters) | 816,703 | ± 0.20% | 99 |
@phensley/messageformat | 547,295 | ± 0.09% | 98 |
@messageformat/core | 185,577 | ± 0.11% | 94 |
@onigoetz/messageformat (+ @onigoetz/make-plural) | 154,576 | ± 0.06% | 101 |
@ffz/icu-msgparser (+ custom renderer) | 130,402 | ± 0.16% | 100 |
format-message-parse | 82,462 | ± 0.18% | 98 |
intl-messageformat | 48,205 | ± 2.25% | 88 |
globalize | 26,975 | ± 0.22% | 97 |
With select and plurals
const input = [`
{gender_of_host, select,
female {
{num_guests, plural, offset:1
=0 {{host} does not give a party.}
=1 {{host} invites {guest} to her party.}
=2 {{host} invites {guest} and one other person to her party.}
other {{host} invites {guest} and # other people to her party.}
}
}
male {
{num_guests, plural, offset:1
=0 {{host} does not give a party.}
=1 {{host} invites {guest} to his party.}
=2 {{host} invites {guest} and one other person to his party.}
other {{host} invites {guest} and # other people to his party.}
}
}
other {
{num_guests, plural, offset:1
=0 {{host} does not give a party.}
=1 {{host} invites {guest} to their party.}
=2 {{host} invites {guest} and one other person to their party.}
other {{host} invites {guest} and # other people to their party.}
}
}
}
`, {
"gender_of_host": "male",
"num_guests": 3,
"host": "Lucifer",
"guest": "John Constantine"
}];
// Renders: `
Lucifer invites John Constantine and 2 other people to his party.
`
Name | ops/sec | MoE | Runs sampled |
---|---|---|---|
@onigoetz/messageformat (+ @phensley/plurals) | 181,594 | ± 0.09% | 97 |
@onigoetz/messageformat (+ make-plural) | 180,194 | ± 0.09% | 102 |
@onigoetz/messageformat (+ @onigoetz/intl-formatters) | 173,668 | ± 0.46% | 97 |
@onigoetz/messageformat (+ @onigoetz/make-plural) | 90,012 | ± 0.65% | 99 |
@phensley/messageformat | 43,007 | ± 16.74% | 82 |
@messageformat/core | 31,066 | ± 0.07% | 97 |
@ffz/icu-msgparser (+ custom renderer) | 28,826 | ± 1.91% | 94 |
intl-messageformat | 17,029 | ± 0.86% | 93 |
format-message-parse | 17,317 | ± 2.62% | 95 |
globalize | 8,653 | ± 1.83% | 97 |