Slugify
Green · 2026-04-21 Node + Browser TextUnicode-aware slugification. Wraps deunicode + unicode-normalization.
- Targets
- Node + Browser
- Version
- 0.1.0
Install
pnpm add @amigo-labs/slugifyBenchmarks
Trend (7 pts)Benchmark
slugify - short ASCII (20 chars)
- @amigo-labs/slugify napi 1.37M hz · 2.84×
- slugify (npm) 483.12K hz
Benchmark
slugify - long ASCII (500 chars)
- @amigo-labs/slugify napi 120.54K hz · 5.50×
- slugify (npm) 21.91K hz
Benchmark
slugify - unicode heavy
- @amigo-labs/slugify napi 275.09K hz · 2.61×
- slugify (npm) 105.42K hz
README
@amigo-labs/slugify
Blazing fast slugify powered by Rust via NAPI-RS. Converts any string into a URL-friendly slug with Unicode support.
Installation
npm install @amigo-labs/slugify
Same install command for Node and the browser. Bundlers select the
right artifact via conditional exports: Node consumers get the
NAPI-RS binary; browser consumers (Vite, esbuild, webpack ≥ 5,
Angular CLI, Bun) get a WebAssembly build with the same JavaScript
API. No separate -wasm package.
Local development. The WebAssembly artifact in
wasm/pkg/is build-time output (gitignored).prepublishOnlybuilds it beforenpm publish, so published tarballs always contain it. For in-tree work (workspace consumers,pnpm pack, local linking) runpnpm build:wasmfirst.
Usage
import { slugify, slugifyWithSeparator } from "@amigo-labs/slugify";
slugify("Hello World!"); // "hello-world"
slugify("Schöne Grüße"); // "schone-grusse"
slugify("日本語テスト"); // "ri-ben-yu-tesuto"
// With custom separator
slugifyWithSeparator("Hello World!", "_"); // "hello_world"
Install for the browser
The same import works in Angular, React, Vite, esbuild, and webpack ≥ 5 — the bundler picks the WASM build via the browser conditional export:
import { slugify, slugifyWithSeparator } from "@amigo-labs/slugify";
The _slugify-core crate is the same code on both sides, so output is identical between Node and the browser.
API
slugify(input): string
Converts a string into a URL-friendly slug using - as separator.
slugifyWithSeparator(input, separator): string
Converts a string into a URL-friendly slug using a custom separator.
Supported Platforms
| Platform | Architecture |
|---|---|
| Linux | x64 (glibc), x64 (musl), arm64 |
| macOS | x64, arm64 |
| Windows | x64 |
License
MIT
Perf review
Perf-Review: @amigo-labs/slugify
Status: 🟢 Green · Reviewed: 2026-04-21 · Version: 0.1.0
Verdict
2.27×–4.49× vs. slugify npm across all input sizes. Unicode normalization + transliteration is real work, and the FFI overhead is relatively small compared to the Rust compute volume. The deunicode crate + unicode-normalization crate do per-character work in pre-computed tables — the JS upstream uses a large JS lookup table plus manual NFD normalization. Cleanest Green shape in the portfolio next to jwt: string-in, string-out, no state, no callbacks, substantial compute.
Classification rationale
- Unicode normalization is the structural win. NFD normalization requires lookups in compact Unicode tables (10k+ entries). Rust’s
unicode-normalizationcrate has hand-optimized decomposition tables (compile-time PHF); JS does the same with a large JS object + a V8 hashmap lookup per char. - Transliteration via
deunicodeis lookup-table performance. A per-char Rust match is faster than JS’s chain-of-switchor map lookup. - Scales linearly with input size. 20-char: 2.82×, 500-char: 4.49×, unicode-heavy: 2.27×. No bimodality.
- Binary size is the trade-off. slugify brings a ~966 KB binary for a 21 KB JS alternative (
docs/perf-review.md:115). This is explicitly documented as an acceptable trade-off (3 orders of magnitude speedup for 3 orders of magnitude bundle).
Evidence
Measured speedup (docs/data.json, 2026-04-18)
| Scenario | @amigo-labs/slugify | slugify npm | Speedup |
|---|---|---|---|
| short ASCII (20 chars) | 1 435 759 Hz | 508 504 Hz | 2.82× |
| long ASCII (500 chars) | 116 529 Hz | 25 966 Hz | 4.49× |
| unicode heavy | 278 351 Hz | 122 440 Hz | 2.27× |
Realistic use-case
URL slug generation — post title → URL path. Typically 10–80 chars of input, one call per post/article/item create. Filename-safe naming for uploads. Database record identity generation. All single-call-per-operation, no hot loops.
Benchmark gaps
- Language matrix not isolated.
deunicodehas language-specific transliteration (German Ü → ue, Chinese 汉字 → Han Zi). One corpus per language would be instructive, but not critical. - Separator-option variation not measured (
_vs-vs custom). - Strict-mode option (alphanumeric-only) not isolated.
API surface
From the crates/slugify/package.json amigo block and typical slugify-npm parity:
slugify(string, options?) → string- Options:
replacement(separator),remove(regex/set of chars),lower,strict,locale
No NAPI class, nothing stateful, no async (trivially fast, no need).
Bundle / binary size
~966 KB per target (explicit in docs/perf-review.md:115). Large relative to the JS alternative (21 KB) because the Unicode tables are embedded. This is the whole portfolio trade-off argument: 3× – 6× speedup for 46× bundle size.
For serverless deployments this matters — 966 KB × 6 targets is significant. For Docker containers it is irrelevant.
FFI-overhead baseline
- 20-char ASCII: string-input UTF conversion ~100 ns, output similar. Rust ~650 ns slugify (incl. normalize+transliterate+join). Total ~1 µs. FFI ~20 % share, but the JS upstream is itself 2 µs per call → we win.
- 500-char: FFI ~500 ns on ~8 µs Rust = 6 %.
- Unicode-heavy: FFI ~500 ns on ~3.5 µs Rust = 14 %.
Green everywhere.
Phase-C optimization checklist
| # | Lever | Applicable | Notes |
|---|---|---|---|
| C.1 | Input-type minimization (&str instead of String) | 🟡 marginal | &str would be possible, saves ~50 ns per call. Sub-percent gain at current margins |
| C.2 | Output-type minimization | ❌ not applicable | String output is natural |
| C.3 | Batch API (slugifyMany) | 🟡 potential | CSV import with slug generation could benefit. Not measured |
| C.4 | Stateful API | ❌ not applicable | Options are lightweight per call |
| C.5 | Parallelization | ❌ not applicable | Single-string single-threaded |
| C.6 | Algorithm swap | ❌ not applicable | deunicode + unicode-normalization are best-in-class |
| C.7 | Allocator tuning (pre-alloc output capacity) | 🟡 marginal | String::with_capacity based on input length × 1.5 — micro-optimization |
| C.8 | Bundle-size | ⚠️ accepted trade-off | Unicode tables are the bulk; not reducible without a scope cut (e.g. making transliteration optional) |
Action plan
Keep-as-is. Green across all scenarios, no open front.
Maintenance:
- Extend the bench matrix (languages, separator options, strict mode).
slugifyManybench if the batch lever becomes portfolio-relevant.- Binary-size reduction as a fast-follow if serverless users complain: a feature-gated
deunicode(ASCII-only / Latin-only / full) could save 60–70 %. No demand so far.
References
- Crate:
crates/slugify - Bench:
crates/slugify/__bench__/index.bench.ts - Lib:
crates/slugify/src/lib.rs - Cargo:
crates/slugify/Cargo.toml - Bundle trade-off discussion:
docs/perf-review.md:115 docs/packages.jsonspeedup:"2.3–4.5× faster"