cljs-dev 2025-11-04

@dnolen I did a test with :lite-mode. Sadly I didn't get any smaller output with :optimizations :advanced but I did find one bug compared to non-lite:

(js/Set. ["checked" "disabled" "selected" "value"])
This works in non-lite but in lite you'll get an error about object not having Symbol/iterable

If you want to test the project I was working with yourself: https://github.com/borkdude/reagami/tree/main/scratch/cljs-app

clj -M -m cljs.main --target browser --output-dir public/js --compile-opts '{:asset-path "js" :lite-mode true :optimizations :advanced}' --compile my-app.core

file a bug, yeah the lite data structures don't do JS iterable yet - but trivial problem

your are using printing machinery - that's usually game over for lite-mode for the obvious reasons

in lite mode, printing is fine for dev - but for release artifact not a good idea

oh you mean (prn :init)?

yeah I can remove that :)

but you also need :elide-to-string

:elide-to-string true

it's worth scanning the output w/ :pseudo-names true to see where you missed something about your implicit dependencies

there is one dependency, the reagami.core namespace, but I can try to disable that one first

the namespaces matter less than some pattern you are using

that goes from 124kb -> 107kb

that reveals that there might be some issue w/ reagami but that's not neccessarily the only problem

I disabled reagami

I can bisect by commenting out stuff

this adds 60kb:

(defonce state
  (atom {:snake [[5 10] [4 10] [3 10]] ; initial snake body
         :dir [1 0] ; moving right
         :food [15 10] ; initial food
         :alive? true}))

this adds another 45kb:

(.addEventListener js/window "keydown"
  (fn [e]
    (let [key (.-key e)]
      (swap! state update :dir
                         (fn [dir]
                           (case key
                             "ArrowUp" (if (= dir [0 1]) dir [0 -1])
                             "ArrowDown" (if (= dir [0 -1]) dir [0 1])
                             "ArrowLeft" (if (= dir [1 0]) dir [-1 0])
                             "ArrowRight" (if (= dir [-1 0]) dir [1 0])
                             dir))))))

that's pretty much the repro

just commenting stuff out can be super misleading

like make an atom and swap it and it and js/console.log the output

if that doesn't result in a size change, then what you observe is a symptom but not the cause

also you eyeball the generated JS for these bits - so see anything suspicious

yeah this gives 60kb:

(def state (atom 0))

(swap! state inc)

huh looking at Atom now

I didn't look at it much when I did the :lite-mode stuff

let me paste the pseudo-names output

it's likely some protocol connectedness issue

no, there really no such thing as protocol connectednes problems wrt DCE

the problem is doseq

for is also generally a problem - they both do too many things

the use of doseq in Atom is gratuitous

but Atom also depends on HashMap, also probably gratuitous

for the watches right?

could just use a JS object perhaps

no, key has to be preserved

but a js/Map could work

but that's too new probably

so when you use lite-mode the standard lib isn't compiled itself with the copy on write stuff?

it is compiled to use lite-mode for literals and some simple constructors because that is generally sufficient

but that doesn't help when the standard lib relies on previously defined to build some later functionality

it's nice from a LISP-y point of view, but not so great for :lite-mode

note though atom is an interface so you could trivially avoid this one by providing your own atom w/o the problem

not sure about this one though, because too many expectations about atom.

for/doseq also not obvious, part of the reason they are so annoying is lack of information at compile about the type of thing

maybe just doseq -> run!

I think doseq could be simplified if there's only one binding, which is very common, to emit run! instead but maybe that's a bit too hacky

(doseq [i [1 2 3]]
  (doseq [j [4 5 6]]
    (prn [i j])))

=>

(run! (fn [i]
        (run! (fn [j] (prn [i j])) [4 5 6])) [1 2 3])

I think so, :lite-mode is really kind of a tactical thing if you already know what you are doing. I think as you want to use more and more of the standard library it's going to be hard to take advantage of :lite-mode - diminishing returns.

regular ClojureScript if you're not using external deps is not big, ~20K gizpped

the mental overhead of :lite-mode just isn't worth the savings.

only if you're doing something very specific, static websites, trivial scripting.

one place where I think it might be useful that you can't really do right now is low level CLJS libs you would otherwise write in JS

that's what I made squint for :)

but it's not so hard to write low level JS in CLJS using aset etc. but it's kind of foot-gunny if you pull in doseq etc out of habit

perhaps a :warn-on-persistent-datastructure or so could help for :lite-mode

Yeah squint is great for that - but when doing this kind of work being able to get at GCL for some things is awesome - no other deps.

At work I have a bunch of Light DOM web components, the output ~8-9K brotli - not very Clojure-y - but I don't have to write it in JS by hand, macros, yadda

haha. I wrote reagami in "idiomatic" squint first, but ended up very imperative style CLJS to make it more performant. Totally not clojury, but if someone uses it you could just do Clojure style without noticing

still, atom , doseq for - worth pondering there might be a way.

for a while there I didn't think :lite-mode could even really work, so probably there are some clever routes here that don't break a bunch of expectations

this is what doseq does in squint: https://squint-cljs.github.io/squint/?src=gzip%3AH4sIAAAAAAAAE9NIyS9OLVSIzlSINlQwUjCO5VKAAqvyjNQ8BY38lBR7hUxNkLhGQVGeQqamJgBptSxzNAAAAA%3D%3D What is the reason doseq pulls in too much stuff? It mostly works on first/rest etc without relying on persistent data structures directly?

doseq and for both know about chunking - that was a big fix just getting rid of that

but for the single seq iteration case I haven't dug into why it's a code size pitfall.

it seems just calling first results into the same bloat the a single doseq gives:

(js/console.log (first []))

I mean 50kb JS output, same with doseq

it's not first though, probably seq is sufficient

(js/console.log (seq [])) here is 15K

need to be careful w/ your flags, I think?

(no compression here)

(js/console.log (atom {})) is 26K no compression

these are my flags:

$ clj -M -m cljs.main --target browser --output-dir public/js --compile-opts '{:asset-path "js" :lite-mode true :elide-to-string true :optimizations :advanced}' --compile my-app.core  && ls -lat public/js

50247  4 nov. 19:38 main.js

hmm when I delete public/js first:

15424  4 nov. 19:39 main.js
That's weird!

could it be it's re-using some old analysis which isn't valid anymore?

doseq now 11kb. that's better

this one's triggering 85kb:

(js/console.log (let [{:keys [a b]} {:a 1 :b 2}]
                  [a b]))

triggered by:

(js/console.log (cljs.core/--destructure-map {}))

ah right, that pulls in persistentarraymap

that's a bug though

destructure doesn't need to specify the map type

do you mean it could do this instead?

cljs.user=> (.createAsIfByAssoc (.-constructor {}) #js [1 2 3 4])
{1 2, 3 4}

that's clever, though ObjMap/HashMap might be missing the needed static method here

code paths go through PAM or PHM w/o explicit request from user should be considered bugs

if you refer to PAM/PHM you'll pull in almost everything

re: the stuff from before, flipping :lite-mode isn't a cache busting compiler flag - so you probably just weren't observing anything at all.

maybe it should be?

good point, fixed in master