Plate 81
scule vs change-case: 570 B vs 795 B Gzip
Aditya Challa4 min read
I bundled the two common string-case helpers on this box: scule 1.3.0 came out at 570 bytes gzip for six converters, while change-case 5.4.4 came out at 795 bytes. On the same fixtures, camelCase took about 0.91 µs per call on scule versus about 1.26 µs on change-case (~1.4×).
Short answer: pick scule when you want a smaller unjs-style helper for kebab/snake/camel identifiers. Keep change-case when you need space-separated title/sentence/constant cases, or when your inputs are human phrases with spaces. No affiliate links in this post.
What I tested
- Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 01:50 IST.
- Versions: scule 1.3.0, change-case 5.4.4, Node 22.20.0, esbuild 0.28.2 browser ESM minify + gzip -9.
- Inputs: 10 identifier-like fixtures (kebab, snake, camel, acronyms, CONSTANT_CASE). Speed: 500,000 calls per function, median of 9 rounds, two runs with library order reversed. Scripts in /workspace/bench-scule-change-case/.
- Findings: core six-export gzip 570 B vs 795 B; camel-only 412 B vs 668 B. camelCase ~914 / 878 ns vs ~1,259 / 1,225 ns; kebab ~890 / 763 vs ~1,169 / 1,138; snake ~851 / 770 vs ~1,360 / 1,132; pascal ~866 / 859 vs ~1,253 / 1,255.
- One surprise: scule does not split on spaces. camelCase("hello world") stays "hello world"; change-case returns "helloWorld". Acronyms and digits also diverge (XMLHttpRequest, user_id_42).
- Not tested: browsers, locale-aware casing, non-ASCII scripts, or change-case's splitSeparateNumbers option matrix beyond the defaults.
How much smaller is scule?
| Build (esbuild browser minify) | Minified | gzip -9 |
|---|---|---|
| scule (6 converters) | 1,267 B | 570 B |
| change-case (7 converters) | 1,970 B | 795 B |
| scule camelCase only | 735 B | 412 B |
| change-case camelCase only | 1,285 B | 668 B |
About 1.4× smaller gzip for a comparable core export set. Track client cost the same way as size-limit vs bundlesize. Other unjs helpers in the same size lane show up in pathe vs node:path and defu vs lodash.merge.
Do the outputs match on real identifiers?
| Input | scule camelCase | change-case camelCase |
|---|---|---|
| foo-bar-baz | fooBarBaz | fooBarBaz |
| Already_Mixed-case | alreadyMixedCase | alreadyMixedCase |
| hello world | hello world (unchanged) | helloWorld |
| XMLHttpRequest | xMLHttpRequest | xmlHttpRequest |
| getHTTPResponse | getHTTPResponse | getHttpResponse |
| user_id_42 | userId42 | userId_42 |
Kebab and snake agree on delimiter-separated identifiers. Spaces, acronyms, and trailing digits do not. change-case also ships capitalCase, constantCase, sentenceCase, pathCase, and friends that scule does not. For broader lodash-style utility swaps, see es-toolkit vs lodash.
Is camelCase speed a reason to switch?
| Call cost (500k, median of 9, 2 order-reversed runs, ns/op) | scule | change-case |
|---|---|---|
| camelCase | 914 / 878 | 1,259 / 1,225 |
| kebabCase | 890 / 763 | 1,169 / 1,138 |
| snakeCase | 851 / 770 | 1,360 / 1,132 |
| pascalCase | 866 / 859 | 1,253 / 1,255 |
scule won every converter I timed by roughly 1.3–1.6×. That matters for codegen loops and build plugins, not for a handful of className transforms. If your bottleneck is hashing keys instead of casing them, compare ohash vs object-hash. Config and path helpers in the same unjs stack sit next to yaml vs js-yaml and smol-toml vs @iarna/toml.
Which one should you pick?
| Situation | My pick |
|---|---|
| Client bundle, kebab/snake/camel only | scule |
| Nuxt / unjs stack already present | scule |
| Inputs are human phrases with spaces | change-case |
| Need CONSTANT_CASE, sentenceCase, pathCase | change-case |
| Must match change-case acronym/digit quirks in tests | stay on change-case |
Bottom line: scule 1.3.0 was 570 B gzip versus 795 B for change-case 5.4.4, and about 1.4× faster on camelCase on this box. Who should not switch: pipelines that feed space-separated titles into the converter, or suites pinned to change-case's acronym and digit rules. Dependency cleanup next door often pairs with knip vs depcheck.
How this was made: I installed both packages on the ShopperCove box, measured esbuild browser bundles, timed four converters in two order-reversed runs, and compared split/camel outputs on seven fixtures. The JSON results sit next to the scripts. The write-up was drafted with AI help and checked against that output.
Sources
- https://github.com/unjs/scule
- https://github.com/blakeembrey/change-case
- https://www.npmjs.com/package/scule
- https://www.npmjs.com/package/change-case
Related
- https://www.shoppercove.com/blog/pathe-vs-node-path
- https://www.shoppercove.com/blog/defu-vs-lodash-merge
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/es-toolkit-vs-lodash
- https://www.shoppercove.com/blog/ohash-vs-object-hash
- https://www.shoppercove.com/blog/yaml-vs-js-yaml
- https://www.shoppercove.com/blog/smol-toml-vs-iarna-toml
- https://www.shoppercove.com/blog/knip-vs-depcheck
Lab evidence
What I found running this
Hands-on on ShopperCove box 6 Oct 2026 ~01:50 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 22.20.0). scule 1.3.0 vs change-case 5.4.4, esbuild 0.28.2 browser ESM minify + gzip -9. Size: scule core 1,267/570 B vs change-case 1,970/795 B; camel-only 735/412 vs 1,285/668. Speed (500,000 calls, 9-round median, order-reversed, ns/op): camelCase scule 913.8 / 878.2 vs change-case 1,259.3 / 1,224.7; kebab 890.4 / 763.1 vs 1,168.6 / 1,137.7; snake 851.1 / 769.9 vs 1,360.0 / 1,132.1; pascal 865.6 / 859.1 vs 1,253.2 / 1,254.7. Behavior: scule leaves "hello world" unsplit; change-case → helloWorld; acronym/digit outputs differ on XMLHttpRequest, getHTTPResponse, user_id_42. Not tested: browsers, locale casing, non-ASCII, splitSeparateNumbers options. No affiliate.