This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-04-06
Channels
- # announcements (14)
- # babashka (14)
- # beginners (22)
- # calva (56)
- # cider (20)
- # clerk (8)
- # clj-commons (10)
- # clj-kondo (18)
- # cljs-dev (11)
- # clojure (87)
- # clojure-conj (3)
- # clojure-europe (29)
- # clojure-nl (1)
- # clojure-poland (5)
- # clojure-portugal (1)
- # clojurescript (100)
- # data-science (3)
- # datahike (1)
- # datomic (13)
- # events (2)
- # fulcro (10)
- # funcool (2)
- # helix (19)
- # hoplon (6)
- # humbleui (2)
- # hyperfiddle (40)
- # leiningen (5)
- # lsp (22)
- # malli (26)
- # nrepl (2)
- # off-topic (19)
- # reagent (32)
- # releases (1)
- # shadow-cljs (266)
- # spacemacs (6)
- # tools-build (9)
- # vim (1)
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 😅
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.
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.
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
my hot reload times average at about 100ms. dunno where vite is at but feels fast enough for me
closure compiler is not involved in hot-reload at all, so it affects nothing in that case
and no your understanding of hot-reload is wrong. shadow-cljs also only sends the updated "modules" to reload, not everything.
I have never seen vite hot-reload in action, so no clue how fast it actually is or how it compares technically
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)
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
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.
that is entirely something your project is doing then. which could probably be optimized by moving some code around.
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
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
> 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.
I'm seeing everything between Compiled in 0.088 seconds.
and Compiled in 0.246 seconds.
depending on file size and affected other sources
just curious if its the actual compilation that is slow, or the rendering client side
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
thats the only part I have any control over. I cannot control how fast react renders your code.
Sure, I was testing if react updating the page was the slow part. but it doesn't look like it is
if it already takes 2 seconds to compile your code that might be something I can look into
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)
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)
https://code.thheller.com/blog/shadow-cljs/2019/08/25/hot-reload-in-clojurescript.html
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
hmm no ssd? dunno why that is taking so long. I get like a 1/10 of that for larger files?
yes, shadow-cljs does nothing react specific. therefore it cannot be as fast as tools that do react specific stuff
I also still don't know how react-refresh works exactly 😛 I haven't used react in many years
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
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.
but regardless. the fact the closure compiler is used doesn't affect anything hot-reload whatsoever
because that literally only does what you tell it to do, but its a whole different workflow
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 😄
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 😛
@U2FRKM4TW didn't you experiment with react-refresh
?
IMHO we need better tools for UI REPL dev. I've been working on some at work internally, might open source something soon(ish)
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
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.
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
unless you are using a bunch of cljsjs packages just trying shadow-cljs to compile should be quick
happy to help if you have questions, I'm always curious to hear how comparisons work out 😛
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
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.
@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
Huh? Why would you need to reload your browser? Just open the DevTools panel and see the Console tab there.
@U02F0C62TC1 you mean somethig like this (ajax.core/GET "
but that too shows me "my response nil"
i tried it, this should work: (ajax.core/GET "https://jsonplaceholder.typicode.com/todos/1" {:handler #(println "my response" %)})`
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.
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
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
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
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.
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.
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.
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.
Interesting, I was playing around with malli yesterday
@U04TGAP892N what would you like to see in malli to make it better for your use case?
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
@UMMMKKADU nothing built-in specific for tests. Could be.
@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.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 😆)
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.