Fork me on GitHub
#clojurescript
<
2023-04-06
>
alexdavis13:04:23

I have recently been bogged down with annoying things involving the closure compiler recently and have been wondering why CLJS still uses it... It seems like the main advantage of the closure compiler is the very efficient advanced compilation, however that compilation only works for 'closure compatible' code, which seems pretty rare in the modern js community. therefore to actually take advantage of this advanced compilation you have to either find closure compatible js libraries or not use anything from the js ecosystem and write everything in native cljs. I'm sure some projects don't need many js libraries and can make do with mostly using CLJS dependencies, but every project I've worked on in the last year or two has faced the following issues: • large bundle sizes because you cannot use advanced compilation for NPM deps (and most of my projects these days seem to need a lot of these for things like auth/tables/graphs/ui components etc) • certain libs that use modern js features will not compile at all even with no optimisations, and your only options are monkey patching the code in node_modules or using a second bundler step, both of which suck • very slow hot code reloading compared to typescript projects, also you often lose state when code hot reloads. closure compiler probably isn't to blame here, but you don't get this issue if you use something like borkdudes squint/cherry alongside esbuild/vite • Various ESM issues (though there are plenty of these over in js land too) I'm sure that the closure compiler was the best tool for the job at the time (and I'm aware CLJS has been around a lot longer than the vast majority of modern JS tools), and perhaps I am just unaware of some cool things it does that nothing else can do, but it does seem like a new CLJS compiler that takes advantage of all the ESM innovations (like esbuild) would make life a much better place 😅

thheller13:04:04

yes, CLJS still uses the closure compiler. :advanced is still unmatched, and is substantially better than regular JS tool "tree shaking". yes, it doesn't work for npm dependencies, but running them through simple matches basic JS tool performance (minus tree shaking though). shadow-cljs does this, cljs/closure :advanced, npm :simple.

thheller13:04:32

rewriting the whole CLJS compiler is an option still, just nobody has done it yet 😉

thheller13:04:24

there is also always the option to run all npm code though webpack (or whatever JS tool), and just CLJS through :advanced. good compromise also.

alexdavis14:04:53

Thanks! I'm guessing you're referring to option 2 from https://code.thheller.com/blog/shadow-cljs/2020/05/08/how-about-webpack-now.html#option-2-js-provider-external in your last point? That is definitely useful and I am doing it on some projects, though it is a bit of a pain to have two bundlers sometimes... Do you think there is any way to get to Vite levels of hot reload speed without moving away from closure compiler? afaik its only possible for it to be so fast because it sends native module files to the browser which can be individually updated, whereas shadow compiles all cljs into a single js file and sends the whole thing. my level of understanding for this stuff is not great though

thheller14:04:32

my hot reload times average at about 100ms. dunno where vite is at but feels fast enough for me

thheller14:04:46

closure compiler is not involved in hot-reload at all, so it affects nothing in that case

thheller14:04:31

yes, I meant :external

thheller14:04:59

and no your understanding of hot-reload is wrong. shadow-cljs also only sends the updated "modules" to reload, not everything.

thheller14:04:15

I have never seen vite hot-reload in action, so no clue how fast it actually is or how it compares technically

alexdavis14:04:56

So not exactly scientific but I have two similarly sized projects (~15k LOC) one in CLJS one in typescript with vite. making any change to the vite one results in the browser updating instantly to my eyes, not actually sure of the best way to time it but lower than 100ms sounds about right. On the CLJS one its at least 2 seconds, sometimes longer (I guess if my computer is busy doing something, or maybe theres some caching layer). For small CLJS projects I never have much of an issue with hot reload speeds its only a problem when things grow (which is why I assumed shadow was sending everything)

alexdavis14:04:01

I have started a new project with uix which uses https://www.npmjs.com/package/react-refresh and that is much quicker, so probably vite is using some similar magic. though the uix project is still relatively small so hard to compare with the big ones

p-himik14:04:11

Are you sure you're not conflating hot code reload with whatever else is associated with it? E.g. it's one thing to just replace the code, and it's a separate thing to e.g. clear re-frame subscription cache and re-render the whole app and anything else one might be doing in ^:dev/before-load and ^:dev/after-load functions.

thheller14:04:43

> On the CLJS one its at least 2 seconds

thheller14:04:19

that is entirely something your project is doing then. which could probably be optimized by moving some code around.

thheller14:04:54

the hot-reload time shouldn't be affected by project size at all.

thheller15:04:06

unless you are touching a file that every other namespace is using or something

alexdavis15:04:24

Yes I think your right, when I think 'hot code reload' I am literally just talking about the time between saving my code change and seeing the rendered components update in the browser. So probably I'm using the wrong terms here

thheller15:04:42

no, thats the time I mean as well.

thheller15:04:03

I mean I believe we went over this before. I'd be curious to see where the 2sec go on your machine. what does the log look like the UI shows for your build? ie. http://localhost:9630/builds

thheller15:04:10

pick your build and make a change

p-himik15:04:45

> no, thats the time I mean as well. That wouldn't be right though - e.g. I have an app that takes 2 seconds just to render a specific page with a huge amount of data. So every instance of hot code reloading also takes those 2 seconds on that page.

thheller15:04:40

I'm seeing everything between Compiled in 0.088 seconds. and Compiled in 0.246 seconds. depending on file size and affected other sources

thheller15:04:19

well, I cannot comment on react render times. I do no factor those in 😛

thheller15:04:30

just curious if its the actual compilation that is slow, or the rendering client side

alexdavis15:04:21

Ok so just did another test, I opened the websocket tab and pressed save as soon as I saw the ping/pong at 16:03:45, it actually took almost a second before the 'build start' message comes in

thheller15:04:45

that info is useless to me. the UI shows all the compilation stuff

thheller15:04:14

thats the only part I have any control over. I cannot control how fast react renders your code.

alexdavis15:04:36

Sure, I was testing if react updating the page was the slow part. but it doesn't look like it is

thheller15:04:44

if it already takes 2 seconds to compile your code that might be something I can look into

alexdavis15:04:57

the build compiles in about a second, and there is another second lost somewhere else

Build completed. (1322 files, 26 compiled, 0 warnings, 1.06s)

thheller15:04:09

again ... please look at the UI. it shows more details.

alexdavis15:04:42

oh you mean the shadow ui?

alexdavis15:04:45

I forgot about that

thheller15:04:58

1sec already seems rather slow, but 26 compiled also seems like a lot

alexdavis15:04:56

Ok yes sometimes it only compiles 2 files and is done in 0.2-0.3 seconds, sometimes I get this

Compiled in 0.782 seconds.
92 Log messages
Resolving Module: :main (14ms)
Resolving Module: :excel (2ms)
build target: :browser stage: :compile-prepare (3ms)
Compile CLJS: app/hooks_utils.cljs (12ms)
Cache write: app/hooks_utils.cljs (32ms)
Compile CLJS: app/help_pages/chart_range.cljs (1ms)
Compile CLJS: app/help_pages/appendix_two.cljs (2ms)
Compile CLJS: app/audit.cljs (8ms)
Compile CLJS: app/events/channel_status.cljs (19ms)
Compile CLJS: app/ag_grid_components.cljs (19ms)
Compile CLJS: app/grid_settings_view.cljs (26ms)
Compile CLJS: app/components.cljs (72ms)
Compile CLJS: app/data.cljs (56ms)
Compile CLJS: app/brent.cljs (75ms)
Cache write: app/help_pages/appendix_two.cljs (126ms)
Compile CLJS: app/help_pages/column_settings.cljs (2ms)
Cache write: app/help_pages/chart_range.cljs (133ms)
Compile CLJS: app/help_pages/creating_new_curve.cljs (3ms)
Cache write: app/audit.cljs (146ms)
Compile CLJS: app/help_pages/creating_new_page.cljs (2ms)
Cache write: app/events/channel_status.cljs (154ms)
Compile CLJS: app/help_pages/eod.cljs (1ms)
Cache write: app/ag_grid_components.cljs (161ms)
Compile CLJS: app/help_pages/excel.cljs (1ms)
Cache write: app/grid_settings_view.cljs (159ms)
Compile CLJS: app/help_pages/graphs.cljs (1ms)
Cache write: app/data.cljs (154ms)
Compile CLJS: app/help_pages/hfc.cljs (6ms)
Cache write: app/components.cljs (183ms)
Compile CLJS: app/help_pages/highlighter.cljs (2ms)
Compile CLJS: app/events.cljs (158ms)
Cache write: app/help_pages/creating_new_curve.cljs (158ms)
Compile CLJS: app/help_pages/row_settings.cljs (2ms)
Cache write: app/help_pages/column_settings.cljs (169ms)
Compile CLJS: app/help_pages/spreader.cljs (1ms)
Cache write: app/help_pages/creating_new_page.cljs (170ms)
Compile CLJS: app/help_pages/time_spreads.cljs (1ms)
Cache write: app/help_pages/eod.cljs (175ms)
Cache write: app/help_pages/excel.cljs (169ms)
Cache write: app/help_pages/graphs.cljs (174ms)
Compile CLJS: app/help_pages/using_broadcast.cljs (21ms)
Cache write: app/help_pages/hfc.cljs (164ms)
Compile CLJS: app/users.cljs (10ms)
Cache write: app/help_pages/highlighter.cljs (182ms)
Cache write: app/brent.cljs (290ms)
Compile CLJS: app/components/product_form.cljs (56ms)
Compile CLJS: app/manage.cljs (58ms)
Cache write: app/help_pages/spreader.cljs (148ms)
Compile CLJS: app/views.cljs (29ms)
Cache write: app/help_pages/time_spreads.cljs (125ms)
Cache write: app/help_pages/row_settings.cljs (156ms)
Compile CLJS: app/core.cljs (22ms)
Cache write: app/components/product_form.cljs (96ms)
Cache write: app/help_pages/using_broadcast.cljs (135ms)
Cache write: app/users.cljs (110ms)
Cache write: app/events.cljs (275ms)
Cache write: app/manage.cljs (110ms)
Cache write: app/views.cljs (138ms)
Cache write: app/core.cljs (147ms)
build target: :browser stage: :compile-finish (0ms)
Flush: app/hooks_utils.cljs (1ms)
Flush: app/components.cljs (1ms)
Flush: app/ag_grid_components.cljs (2ms)
Flush: app/data.cljs (2ms)
Flush: app/help_pages/column_settings.cljs (1ms)
Flush: app/help_pages/appendix_two.cljs (1ms)
Flush: app/help_pages/chart_range.cljs (1ms)
Flush: app/help_pages/creating_new_page.cljs (1ms)
Flush: app/help_pages/creating_new_curve.cljs (2ms)
Flush: app/events/channel_status.cljs (2ms)
Flush: app/help_pages/eod.cljs (1ms)
Flush: app/audit.cljs (2ms)
Flush: app/help_pages/excel.cljs (1ms)
Flush: app/grid_settings_view.cljs (2ms)
Flush: app/help_pages/graphs.cljs (1ms)
Flush: app/help_pages/highlighter.cljs (1ms)
Flush: app/help_pages/time_spreads.cljs (1ms)
Flush: app/help_pages/hfc.cljs (2ms)
Flush: app/help_pages/spreader.cljs (1ms)
Flush: app/help_pages/row_settings.cljs (1ms)
Flush: app/help_pages/using_broadcast.cljs (1ms)
Flush: app/brent.cljs (5ms)
Flush: shadow/module/excel/append.js (1ms)
Flush: app/users.cljs (1ms)
Flush: shadow/module/main/append.js (1ms)
Flush: app/core.cljs (1ms)
Flush: app/views.cljs (1ms)
Flush: app/components/product_form.cljs (2ms)
Flush: app/manage.cljs (2ms)
Flush: app/events.cljs (5ms)
Flushing unoptimized modules (37ms)
build target: :browser stage: :flush (40ms)

thheller15:04:00

ok, define "sometimes"? that looks like it recompiled a whole lot?

thheller15:04:00

that explains how hot-reload works in more detail

thheller15:04:38

Cache write: app/help_pages/spreader.cljs (148ms) how large are these files?

alexdavis15:04:46

seems like its when I touch more than one file maybe? but having a hard time reproducing exactly. Thanks I'll have a read. It does look like shadow is not the problem though because even when shadow takes 0.2s the ui takes 1-2s to update, which I'm guessing is because its calling react render-root on every update (which is also why react state gets lost). So I guess shadow is not trying to be like Vite, it is only concerned with bundling code and shipping it to the browser (like esbuild) whereas vite has a bunch of other concerns like updating react components individually rather than remounting every time

alexdavis15:04:56

that one is 339 lines

thheller15:04:17

hmm no ssd? dunno why that is taking so long. I get like a 1/10 of that for larger files?

alexdavis15:04:42

I'm on an m1 macbook, disk should be pretty fast

alexdavis15:04:53

Tried it again and getting 23ms to cache that file now

thheller15:04:13

hmm yeah that seems more like it

thheller15:04:45

yes, shadow-cljs does nothing react specific. therefore it cannot be as fast as tools that do react specific stuff

👍 2
thheller15:04:22

I also still don't know how react-refresh works exactly 😛 I haven't used react in many years

alexdavis15:04:33

yeah it is also a bit weird because it breaks if you export anonymous functions and has other issues. but when it does work it is very quick and I did grow a bit dependant on having such a fast reload cycle

thheller15:04:01

I get that fast is nice. to me everything takes at most 500ms (including rendering) even if I recompile like half my project. so to me that is very fast.

thheller15:04:23

but regardless. the fact the closure compiler is used doesn't affect anything hot-reload whatsoever

thheller15:04:28

so that is not the factor here

thheller15:04:02

could things be faster: probably.

thheller15:04:19

I mean the ultimate fastest speed possible you get via the REPL

thheller15:04:44

because that literally only does what you tell it to do, but its a whole different workflow

thheller15:04:51

and IMHO not as nice as automated hot-reload 😛

alexdavis15:04:28

I guess speed is secondary to loosing state. not an issue with pure reagent/reframe + atoms but with newer react libraries and hooks remounting everything resets the state which can be really painful. but yeah I think I was unfairly putting blame on the wrong tools and I think I can solve the state issue by figuring out how react-refresh works and yeah if I could re-eval a component and have the ui update that would be super cool 😄

thheller15:04:53

yeah can't comment on that part. shadow-cljs just calls the :dev/after-load and is done as far as it is concerned. it knows nothing about react/reagent/re-frame 😛

thheller15:04:31

@U2FRKM4TW didn't you experiment with react-refresh?

p-himik15:04:01

Nah, not me.

lilactown15:04:13

IMHO we need better tools for UI REPL dev. I've been working on some at work internally, might open source something soon(ish)

lilactown16:04:42

react fast refresh is hard to get right. helix has experimental support for it, but it requires some macro logic to determine when to invalidate a component and remount because you've changed some stateful logic inside of it. it gets more complicated with custom hooks that could do the same outside the scope of the component

mikerod14:11:04

Ah ok, that makes sense. Thanks for the refresher. I was reading one of your posts about hot reloading recently. You mention that shadow is faster than figwheel/cljs defaults for one reason being not recompiling all dependents. You instead only compile direct dependents. If this is much faster, any idea why cljs changed the default at one point? I was wondering what the troubles were. Also, is compiling the direct dependents something cljs doesn’t do if you set the :recompile-dependents false? Overall, I’m still wanting to try switching to shadow on one of my big slow projects and start to explore things. It’s just a bit tricky to overhaul right away. It’s on the agenda though.

thheller14:11:20

I don't know enough about regular CLJS/figwheel hot-reload workflows or options. Everything regarding that is entirely custom in shadow-cljs, and its been many years since I looked at other implementations. none of what I said might accurate, no real clue how it works today

thheller14:11:10

unless you are using a bunch of cljsjs packages just trying shadow-cljs to compile should be quick

thheller14:11:38

happy to help if you have questions, I'm always curious to hear how comparisons work out 😛

mikerod14:11:54

no cljsjs here - just using webpack for npm deps.

mikerod14:11:40

Ok. I hope I can try this out in the next week or so. Will see.

bibiki13:04:20

Hi, can anybody explain why (:response (ajax.core/GET "")) returns nil while the result to GET is accompanied with a log line that says CLJS-AJAX response: {userId 1, id 1, title delectus aut autem, completed false} even more importantly, how can I get a hold of the user data? thank in advance

p-himik13:04:37

Because the log line is free to use whatever formatting it wants. Just log the result of (ajax.core/GET ...) and you'll see the issue.

rolt13:04:28

i think it's async, you need to use a callback

bibiki13:04:29

here is the println of the result that GET gives me: #object[Object [object Object]]

p-himik13:04:28

Don't use println, use js/console.log.

bibiki13:04:36

@U2FRKM4TW in a figwheel repl js console.log shows nothing so I use println there to avoid having to reload browser every time I need to see my logs

p-himik13:04:04

Huh? Why would you need to reload your browser? Just open the DevTools panel and see the Console tab there.

bibiki13:04:12

@U02F0C62TC1 you mean somethig like this (ajax.core/GET "" {:handler #(println "my response" (:response %))}) but that too shows me "my response nil"

rolt13:04:39

and without the keyword :response ?

rolt13:04:06

i tried it, this should work: (ajax.core/GET "https://jsonplaceholder.typicode.com/todos/1" {:handler #(println "my response" %)})`

bibiki13:04:16

right, the handler seems to receive the data as argument

p-himik13:04:44

A better approach is to use promises. ajax.core/GET probably returns a promise that you can easily pass around and use via JS interop. And definitely look into how to use js/console.log properly - it lets you discover the data interactively, right in the browser, without having to serialize it while losing any details.

bibiki13:04:39

right, @U2FRKM4TW. printlns show in the browser too. I'll have to try both to see if console logs give me something more than println

rolt13:04:13

no I don't think there's native support for promise in cljs-ajax, but it's not hard to create one and deliver the result in the handler

bibiki14:04:59

in my handler I am putting the results in an atom

bibiki14:04:57

and I think he was rather saying that GET returns a promise that may be delivered when needed, hence avoiding having to provide a handler

rafaeldelboni14:04:06

Hey I'm about to create a backend/frontend project and I'm deciding the stack, does anybody used any schema (plumatic/schema, malli, specs) framework on frontend successfully and have a recommendation? I would like to use the same schema both in the backend and the frontend.

Grigory Shepelev15:04:00

you can store all yours schemas (malli, specs,...) in .cljc files and share them between both frontend and backend. as far as I know all this libs support both clj and cljs.

rafaeldelboni15:04:30

Cool, have you tried any of those in the frontend development? I would like to see if the experience of using those for the frontend is the same for the backend or it changes, tooling wise.

andre16:04:30

I've tried clojure.spec in the past for validation but it doesn't have builtin support for humanized error messages. There are a couple libs from the community but I didn't like them very much. Malli has malli.error/humanize, which is nice, but I had to make some changes to make it behave the way I wanted.

andre16:04:38

I'd probably go with malli again if I needed

andre16:04:24

It wasn't great but it was pretty ok.

rafaeldelboni16:04:55

Interesting, I was playing around with malli yesterday

ikitommi16:04:24

@U04TGAP892N what would you like to see in malli to make it better for your use case?

rafaeldelboni16:04:45

Hey @U055NJ5CC since you here hahaha 😁 Is there an with-fn-validation equivalent for malli? I would like to wrap my unit tests with something like this, current I'm starting and stoping the instrumentation

andre16:04:03

There is, search for function schemas on docs

ikitommi16:04:27

@UMMMKKADU nothing built-in specific for tests. Could be.

andre16:04:39

@U055NJ5CC it was quite some time since I faced those issue, but one thing that was tricky was:

(do

    (require '[malli.core :as m])
    (require '[malli.error :as me])

    (def schema
      [:map
       [:foo [:or string? pos-int?]]
       [:bar [:and string? pos-int?]]])

    (me/humanize (m/explain schema {:foo -1 :bar -1}))

    )
Returns:
{:foo ["should be a string" "should be a positive int"],
 :bar ["should be a string" "should be a positive int"]}
hard to tell the difference looking at the errors if it is an and or if it is an or failure, which makes it hard to present those errors on the UI.

andre16:04:45

Besides that, I felt like malli was not opinionated enough. Like clojure.spec is, by enforcing registering keys, and making schema and required keys different things, with s/keys/`select (spec2)` . It was super easy to extend malli to make it behave the way I wanted, and I love that aspect of malli. Extending clojure.spec is super tricky in my experience (you might agree, given you created spec-tools 😆)

andre16:04:21

But that's not something I'd like to change on malli, guess it is just a matter of taste. I get it why it is the way it is. I just like the design and philosophy of spec.

andre16:04:40

Also, I was surprised that I couldn't just use plain functions as malli schemas, and sometimes some functions from clojure.core would also not work, like even?. int? string? pos-int? all work, but even? doesn't. And I had to wrap my own functions with [:fn ...] IIRC.

andre16:04:24

But my experience was mostly positive, don't take those criticisms as a bad thing. I really don't know what I'd have used instead if malli didn't exist. Thanks for creating and maintaining it, it is an awesome library overall.