cljs-dev 2025-07-30

:lite-mode is looking pretty interesting, 7K brotli / 8K gzip for println + vector

❓ 1

but if we use the same strategy for map and vector and set - I don't think it will grow much beyond that

that is just loading the original data structure implementations from 2011 w/ whatever fixes or modifications they might need

low impact because it's a compiler flag, and adding the old implementation isn't really all that much code

idea is to make the compiler just do something for literals - and then as people play around w/ it - could do compiler pass stuff to fix all cases.

of course if you skip printing because you're just building something simple - initial size will be much smaller than 7K, more like 3K before compression for a vector, using hash maps and sets won't be adding much either

how do you feel about adding access to newer JS features such as spread/rest params? I mean a lot of code is "wasted" on juggling arguments?

💯 1

code like this is pretty common

zm = function (a) {
  switch (arguments.length) {
    case 6:
      return Am(
        arguments[0],
        arguments[1],
        arguments[2],
        arguments[3],
        arguments[4],
        arguments[5]
      );
    case 7:
      return ym(
        arguments[0],
        arguments[1],
        arguments[2],
        arguments[3],
        arguments[4],
        arguments[5],
        arguments[6]
      );
    default:
      throw Error(["Invalid arity: ", v.g(arguments.length)].join(""));
  }
};

zm = function (...a) {
  switch (a.length) {
    case 6:
      return Am(...a);
    case 7:
      return ym(...a);
    default:
      throw Error(["Invalid arity: ", v.g(a.length)].join(""));
  }
};

💯 1

dunno how that actually compares after gzip, but might be worth investigating

🤔 1

I don't really love the idea of relying on newer JS features (we currently have a mistake in the codebase I need to address), it has to be a big win. I did play around w/ sharing the error strings but compression showed it made no difference at all. for repeated stuff like this probably compression fixes it for us.

But, not against considering it if someone wants to do a code size assessment

I implemented this for squint just now: https://clojurians.slack.com/archives/C07UQ678E/p1753867547957739?thread_ts=1753867469.711179&cid=C07UQ678E but I think it will break applying a function to an infinite sequence. e.g.: (apply (fn [x & xs] x) (range)) so I'll likely have to revert this change

should only use the spread thing for fixed multi-arity fns. variadic stuff must have special handling precisely because of apply and seqs

this will complicate the code somewhat though. the outer function:

zm = function (...a) 
can only do this when all the arities are fixed but when there is one varargs, you must fall back to the old impl

hmm why? the variadic bit currently has to do this array copy to get convert arguments, which is then no longer necessary?

demo.browser.x = function demo$browser$x(...a) {
  switch (a.length) {
    case 1:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$1(...a);
      break;
    case 2:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$2(...a);
      break;
    default:
      var argseq__5775__auto__ =
        2 < a.length
          ? new cljs.core.IndexedSeq(a.slice(2), 0, null)
          : null;
      return demo.browser.x.cljs$core$IFn$_invoke$arity$variadic(
        a[0],
        a[1],
        argseq__5775__auto__
      );
  }
};

I mean this would need to be benchmarked to see if the ...a spread is even usefull perf wise. could just stick with a[0] etc, but since the rest thing creates a proper array it should make handling actually easier

demo.browser.x = function demo$browser$x(var_args) {
  var G__45643 = arguments.length;
  switch (G__45643) {
    case 1:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$1(arguments[0]);

      break;
    case 2:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$2(
        arguments[0],
        arguments[1]
      );

      break;
    default:
      var args_arr__5774__auto__ = [];
      var len__5749__auto___45666 = arguments.length;
      var i__5750__auto___45667 = 0;
      while (true) {
        if (i__5750__auto___45667 < len__5749__auto___45666) {
          args_arr__5774__auto__.push(arguments[i__5750__auto___45667]);

          var G__45668 = i__5750__auto___45667 + 1;
          i__5750__auto___45667 = G__45668;
          continue;
        } else {
        }
        break;
      }

      var argseq__5775__auto__ =
        2 < args_arr__5774__auto__.length
          ? new cljs.core.IndexedSeq(args_arr__5774__auto__.slice(2), 0, null)
          : null;
      return demo.browser.x.cljs$core$IFn$_invoke$arity$variadic(
        arguments[0],
        arguments[1],
        argseq__5775__auto__
      );
  }
};
compared to this currently I mean

heck, given that its a proper array we might even be able to change the variadic impl to something like

demo.browser.x = function demo$browser$x(...a) {
  switch (a.length) {
    case 1:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$1(...a);
      break;
    case 2:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$2(...a);
      break;
    default:
      return demo.browser.x.cljs$core$IFn$_invoke$arity$variadic(a, 2);
  }
};
I have spent no time on this, so definitely requires more thought, but I'm fairly confident that it simplifies a lot of code overall

What I mean is, the outer function has:

demo$browser$x(...a) 
wouldn't this convert the infinite lazy seq to an array, which isn't possible in finite time and space?

no? I'm confused, not actually sure what you mean.

let's just start with this example:

(apply (fn [x & xs] x) (range))
how would this be handled by the above? since a range doesn't have a length property, I guess it would hit the default case which then works properly with seqs? I think I got it then

yeah that hits the applyTo helper first, which extracts the first two args and calls the $variadic fn. so should be fine.

👍 1

yeah there's still a way to go (not clear what bumps I'm gonna hit here), buuuuut .... in master which has a large number of changes for code size, 8K brotli for just a vector, 1K brotli in the :lite-mode branch

so people building simple interactive stuff, the cost of CLJS just went waaaaaay down.

😍 2

3K vs 88K for a simple hash-map in the :lite-mode branch

nice work. does equality and hashing etc still work the same way?

it's all protocols so if it's implemented yes

but how do you explain the difference? I mean PersistentHash/ArrayMap are not 85K of code?

oh but actually they are

PHM needs PAM needs Vector needs ChunkedSeq ...

once you hit PHM you pull in everything

PAM pulls in PHM

and the Transient impls

and MapEntry - anyways you get the idea

so it's a mess, so using the old stuff means you just avoid all that

so I see two goals w/ :lite-mode

1. removing DCE issues remaining in the standard lib beyond data structures

2. gauging what people actually need/tolerate in :lite-mode - like just not implementing toString

2 implies giving up on full fidelity, perhaps even compatibility in some cases - maybe that's fine - not going to figure that on day 1 or even probably in a year

so :lite-mode might be for people who just don't care about dependencies - they just want to write some Clojure, get a tiny artifact and go do something else

@dnolen squint was also made to get very small artifacts, so you can actually publish your "CLJS" library to npm without bringing the whole data structure lib + part of the stdlib into it. one extra consideration with squint (and cherry) is that two or more libraries can share the same stdlib - this is done by compiling against a standard library which it itself a library on npm of which you get just one copy. I understand that this may not be a goal of lite-mode, since you want to have nothing to do with npm.

I mean some knobs to tweak the size would definitely be nice. I already have a few in shadow-cljs to shave a few pct here and there.

definitely! this is a big knob for that - it might open the door for others.

I think w/ some sweat thought we can get CLJS <10K compressed for lots of simple programs

squint is also copy-on-write. you will definitely notice the difference e.g. when doing Advent of Code puzzles where in some cases where you're updating a map a million times

Haha, I know, I literally copied this stuff from 2011 commit 🙂 I remember how "fast" CLJS was back then

one of the big benefits here is going to be much like the last exercise - "why does the code size suddenly explode?"

but now you don't have all these persistent data structures that refer to each other muddying the waters

that said - I think getting :lite-mode to parity is going to be a long haul and a non-goal for shipping an experimental flag

it's looking more and more promising

just doing some trivial map, filter, range etc leads sub 10K brotli (like 5-8K)

add the data structures and you're still around 10K

needs a lot more testing, but overall I think this is really cool and overall compelling story

you can just play around w/ :lite-mode and have fun - the cost is marginal if not better than JS alternatives and you don't have to think about depenendencies at all

I was playing around goog.ui.Popup the other day and the positioning stuff, after having used some React stuff I was much more impressed w/ the decomplected nature of GCL

stepping back even further, WebComponents sans Shadow DOM by tying together some GCL is look really cool to me - and diffing whatever is completely optional and can work.