@httpx/memo-intl
v2.1.0
Published
LRU-based memoizer for Intl constructors.
Downloads
4,307
Readme
@httpx/memo-intl
Speeds up Intl operations up to 50x (bun) and 40x (node). LRU-based memoizer for Intl.NumberFormat,Intl.DateFormat and other Intl constructors. About 1kb.
Install
$ npm install @httpx/memo-intl
$ yarn add @httpx/memo-intl
$ pnpm add @httpx/memo-intlFeatures
- 👉 Don't re-create the same Intl instance for the same options (memoized).
- 👉 Keep the Intl api untouched. Just replace
new Intl.NumberFormatbyintl.NumberFormat... - 👉 Up to 50x faster on bun and 40x on node than non-memoized Intl constructors.
- 👉 Decrease memory usage, unwanted memory leaks and garbage collection pressure.
- 👉 Max out 50 cache instances by default with @httpx/lru.
- 👉 Lightweight. Node, bun, browser and edge support.
Documentation
👉 Official website or Github Readme
Upgrading from MIntl? See the UPGRADE guide.
Usage
Create a memoized instance once (ie: at module level) and reuse it across your app.
import { createMIntl } from "@httpx/memo-intl";
// Options are optional, the LRU cache defaults to 50 entries
export const intl = createMIntl({ lru: { maxSize: 50 } });NumberFormat
// Notice: `new Intl.NumberFormat` vs `intl.NumberFormat`
const formattedPrice = intl
.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
notation: "compact",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
.format(row.price);DateTimeFormat
// Notice: `new Intl.DateTimeFormat` vs `intl.DateTimeFormat`
const formatter = intl.DateTimeFormat("fr-FR", {
dateStyle: "full",
timeStyle: "full",
timeZone: "UTC",
});
const date = Date.parse("2024-05-29T07:42:43.230Z");
expect(formatter.format(date)).toBe(
"mercredi 29 mai 2024 à 07:42:43 temps universel coordonné"
);
expectTypeOf(formatter).toEqualTypeOf<Intl.DateTimeFormat>();Locale
const locale = intl.Locale("fr-FR", { caseFirst: "lower" });
console.log(locale.language); // "fr"Collator
const collator = intl.Collator("de", { sensitivity: "base" });
console.log(["Z", "a", "z", "ä"].sort(collator.compare));RelativeTimeFormat
const rtf = intl.RelativeTimeFormat("en", { style: "short" });
console.log(rtf.format(3, "month")); // e.g. "in 3 mos."ListFormat
const listFormatter = intl.ListFormat("en", {
style: "long",
type: "conjunction",
});
console.log(listFormatter.format(["Red", "Green", "Blue"]));PluralRules
const pr = intl.PluralRules("en-US", { type: "cardinal" });
console.log(pr.select(1)); // "one"Segmenter
const segmenter = intl.Segmenter("fr", { granularity: "word" });
const result = segmenter.segment("Bonjour le monde");
console.log([...result].map(({ segment }) => segment));DisplayNames
const regionNames = intl.DisplayNames("en", { type: "region" });
console.log(regionNames.of("BE")); // "Belgium"DurationFormat
Requires a runtime supporting
Intl.DurationFormat(Node 23+, Bun, Chrome 129+, Firefox 136+, Safari 16.4+).
const formatter = intl.DurationFormat("en", { style: "short" });
console.log(formatter.format({ hours: 1, minutes: 46 })); // "1 hr, 46 min"Cache
You can clear the cache or check the cache stats.
// Clear the cache
intl.cache.clear();
// Check cache stats
console.log(intl.cache.stats());Memory usage
Cached instances stay in memory until they're evicted from the LRU cache. Most of their memory is native ICU data held outside the JS heap, so it shows up in the process RSS, not in heap snapshots.
Approximate RSS per cached instance, measured with 5000 instances on macOS arm64:
| Constructor | Node 26.13 | Bun 1.4.4 | | ------------------ | ---------: | --------: | | Locale | ~0.5 KB | ~0.5 KB | | NumberFormat | ~1 KB | ~1 KB | | Collator | ~1.5 KB | ~1.5 KB | | PluralRules | ~4 KB | ~5 KB | | RelativeTimeFormat | ~8 KB | ~8.5 KB | | DateTimeFormat | ~30 KB | ~13 KB |
With the default maxSize of 50, the worst case (50 different DateTimeFormat options) holds about 1.5 MB. Size the cache for the number of distinct locale/options combinations you actually use: a larger cache that is never filled costs nothing, but every distinct entry stays allocated until evicted.
Custom and shared instances
Each createMIntl() call returns an independent instance with its own LRU cache.
MIntl is the default globalThis-shared instance. getOrCreateMIntl returns that instance when called without arguments. To create additional shared instances, pass a stable name; the first call for a name sets its LRU options, and a later call with different options logs a warning (except when NODE_ENV is production). The default LRU size is 50.
import { getOrCreateMIntl } from "@httpx/memo-intl";
const defaultIntl = getOrCreateMIntl();
const intl = getOrCreateMIntl("my-app", { lru: { maxSize: 200 } });Usage in libraries
If you publish a library (ie: a UI kit or a date utility) that depends on @httpx/memo-intl, don't use MIntl or getOrCreateMIntl() without a name. Those return the "default" instance stored on globalThis, which is also the one the app uses. Your formatters would share its LRU slots and evict the app's entries (and the reverse), and a cache.clear() on either side would empty both.
Create a private instance instead, sized for the formatters your library actually needs:
// my-lib/src/intl.ts
import { createMIntl } from "@httpx/memo-intl";
export const intl = createMIntl({ lru: { maxSize: 10 } });If your library is split into several packages or bundles that should share one cache, use getOrCreateMIntl with a name scoped to your package, and pass the same options everywhere:
import { getOrCreateMIntl } from "@httpx/memo-intl";
export const intl = getOrCreateMIntl("@my-scope/my-lib", {
lru: { maxSize: 10 },
});Other recommendations:
- Never call
cache.clear()on an instance you don't own. - Keep
maxSizesmall: each cached entry stays in memory until evicted, up to ~30 KB for aDateTimeFormat(see Memory usage).
Benchmarks
Performance is monitored with codspeed.io.
See bench for details.
Each sample formats 1000 rows and asks for a formatter on every row. hz is the number of samples per second (higher is better).
| Constructor | Runtime | intl.X() (hz) | new Intl.X() (hz) | Speedup |
| --- | --- | --: | --: | --: |
| DateTimeFormat | Node 26.13 | 836 | 30.9 | 27x |
| DateTimeFormat | Bun 1.4.4 | 572 | 12.4 | 46x |
| NumberFormat | Node 26.13 | 2,101 | 55.3 | 38x |
| NumberFormat | Bun 1.4.4 | 2,224 | 41.9 | 53x |
| RelativeTimeFormat | Node 26.13 | 2,466 | 135 | 18x |
| RelativeTimeFormat | Bun 1.4.4 | 2,665 | 125 | 21x |
| Locale | Node 26.13 | 8,404 | 362 | 23x |
| Locale | Bun 1.4.4 | 15,252 | 1,303 | 12x |
Measured with
pnpm benchandpnpm bench-bun(vitest 5.1) on macOS (Apple silicon). Results vary by machine.
Bundle size
Bundle size is tracked by a size-limit configuration
| Scenario | Size with deps (compressed) | | ----------------------------------------- | --------------------------: | | `import { MIntl } from '@httpx/memo-intl' | ~ 1.02kB |
Note that per-se the library weights less than 300 bytes, the size limit accounts for the @httpx/lru dependency.
Compatibility
| Level | CI | Description | | --- | --- | --- | | Node | ✅ | CI for 22.x, 24.x & 26.x. | | Browser | ✅ | Tested with latest chrome (vitest/playwright) | | Browserslist | ✅ | > 95% on 01/2025. defaults, chrome >= 96, firefox >= 105, edge >= 113, safari >= 15, ios >= 15, opera >= 103, not dead | | Bun | ✅ | Tested with latest (at time of writing >= 1.3.3) | | Edge | ✅ | Ensured on CI with @vercel/edge-runtime. | | Cloudflare | ✅ | Ensured with @cloudflare/vitest-plugin (see wrangler.toml | | Typescript | ✅ | TS 5.0 + / are-the-type-wrong checks on CI. | | ES2022 | ✅ | Dist files checked with es-check | | Performance | ✅ | Monitored with with codspeed.io |
For older browsers: most frontend frameworks can transpile the library (ie: nextjs...)
Contributors
Contributions are warmly appreciated. Have a look to the CONTRIBUTING document.
Sponsors
If my OSS work brightens your day, let's take it to new heights together! Sponsor, coffee, or star – any gesture of support fuels my passion to improve. Thanks for being awesome! 🙏❤️
Special thanks to
License
MIT © belgattitude and contributors.
