Feature Highlight: :shadow-keywords

Keywords and Symbols in CLJS code are usually constructed on the fly. Meaning that something like :foo will generate

// Keyword(ns, name, fully-qualified-name, hash-code)
// :foo
new cljs.core.Keyword(null,"foo","foo",1268894036);
// :some.ns/foo ::foo
new cljs.core.Keyword("some.ns","foo","some.ns/foo",123124121);

In release builds those will be replaced by constants meaning that this code will only be called once per keyword and the constant used instead.

Although I wrote the constants optimizer for shadow-cljs (which is not using the built-in :emit-constants) I never realized that the string is always included twice. @cmal in Clojurians made me aware of this and sparked my curiosity on how much difference in compiled output that would make. My instinct said that gzip will take care of this and it more or less does.

Optimized it looks like this

// before renaming
var cljs$cst$keyword$foo = new cljs.core.Keyword(null, "foo", "foo", 1231231);
// after renaming
var X = new Y(null,"foo","bar",1231231);
// vs new version
// before
var cljs$cst$keyword$foo = shadow$keyword("foo", 1231231);
// after
var X = Y("foo",1231231);
// qualified keywords
var X = Z("some.ns","foo",1231231);

Thats already 14 bytes shorter per keyword. In my production app which is split into 4 modules this means a difference of

  • “common” module
  • raw: 413761 bytes -> 406811 bytes
  • gzip: 107458 bytes -> 107477 bytes

It got a few bytes larger. Doesn’t contain many keywords, just the new “helper” functions minified.

  • module A
  • raw: 189617 bytes -> 180687 bytes
  • gzip: 53808 bytes -> 52762 bytes

~1KB saved.

  • module B
  • raw: 59628 bytes -> 54307 bytes
  • gzip: 17404 bytes -> 16784 bytes

~600 bytes saved.

  • module C - no changes since just the JS code from npm without any keywords

Overall a rather small amount of bytes saved and it is now paying the extra function call cost. It might not be worth doing this optimization after all but it might be worth investigating the overall impact for you app.

If you are into these sort of micro benchmarks please report your findings when compiling your build with shadow-cljs@2.0.98+ and :compiler-options {:shadow-keywords true} (false by default).

I expect that the difference will be much larger for reagent apps due to the heavy use of keywords.

@cmal already reported 187304 -> 185424 for his project (gzip’d).

If we get enough real-world reports to determine that this is worth doing we make it the default and maybe transfer this code to cljs.core.


In semantic version, when new features are introduce, the minor version is incremented. In 2.0.98+, it always adds in patch version. Or maybe shadow-cljs is not in semver…

It is not semver. It just has to look like semver since thats what npm/maven want. I don’t believe in semver and just bump by one for every new release I make.

If I make changes that I expect to break someones config I will bump major/minor accordingly. So far that should not have happened since 2.0.

This about matches what I do.

1 Like

shadow-cljs@2.0.99 fixed the accidental debugging leftover console.log. Oops.