Fork me on GitHub
#shadow-cljs
<
2024-03-06
>
geraldodev14:03:42

When you mix shadow-cljs with many js components, how do you tree shake the end result to reduce the bundle size usually ?

thheller15:03:55

shadow-cljs does not perform tree shaking for npm packages, what is "required" (as in :require in ns) will stay

thheller15:03:32

CLJS is tree shaken via closure compiler :advanced

geraldodev15:03:23

I remember you saying this on this group before, that's because I'm asking usually which tool ppl use to reduce JS bundle.

thheller15:03:50

well that depends on what you want to reduce 😛

thheller15:03:13

if you use npm libs that rely on webpack-style tree shaking then you use webpack

thheller15:03:05

i.e. let webpack process npm packages, while shadow-cljs handles the CLJS parts

geraldodev15:03:08

webpack is hairy 🙂

thheller15:03:44

well any js tool that can tree shake will work, doesn't have to be webpack

thheller15:03:35

if you use packages that allow direct requires you can usually just manually tweak a bit to get the same result with just shadow-cljs

thheller15:03:43

but not all JS packages allow that

thheller15:03:01

and if you are not in full control over what is required then it won't work at all

geraldodev15:03:34

"Lucide is built with ES Modules, so it's completely tree-shakable.", its a huge file, that you can use in dev mode, but reducing size the final artifact is necessary.

thheller15:03:12

it does allow direct requires as far as I can tell

geraldodev15:03:55

Yeap, I think so.

rafaeldelboni16:03:41

Do you folks have any example repo using webpack to tree-shake an shadow-cljs project? I'm interested to se the developer experience of using this.

rafaeldelboni16:03:47

(sorry to intrude)

thheller18:03:17

the dev experience is just that you run a second tool alongside shadow-cljs

thheller18:03:37

some restrictions to the REPL and hot-reload apply as JS stuff can no longer be loaded dynamically

thheller18:03:47

other than that its pretty much the same

rafaeldelboni19:03:17

ah ok, I will do some studies 🙂 Thanks

thheller19:03:10

fwiw I tested this without any webpack config at all

thheller19:03:53

can't remember the exact options used but basiscally just input/output and production mode

geraldodev19:03:19

I've switched to esbuild to process jsx. It's easier, than babel + packages/@ui. I've followed https://shadow-cljs.github.io/docs/UsersGuide.html#js-provider-external. Thomas , in theory , is it possible to join lib.js generated from target/index.js and main.js and tree shake ?

:js-options {
                                   :js-provider :external
                                   :external-index "target/index.js"
                                   :external-index-format :esm
                                   }

geraldodev19:03:57

To compile with esbuild I'm using ./node_modules/.bin/esbuild target/index.js --bundle --outfile=resources/public/js/app/lib.js

thheller19:03:46

you can use https://shadow-cljs.github.io/docs/UsersGuide.html#_third_party_tool_integration instead of target :browser and run it completely through esbuild

thheller19:03:14

otherwise no, you cannot and should not join the external file with the regular output

geraldodev19:03:11

With :js-options {:js-provider :import} the modules must be :modules {:demo {:exports {default demo.lib/hello}}} like in the example ? I'm asking because with :target :browser I'm using :modules :modules {:main {:init-fn dulcis.client/init}}

thheller19:03:55

that depends on what you want to build

thheller19:03:58

just :init-fn is fine

🙏 1
thheller19:03:25

:exports creates actual ESM export, so in case you want to use the output from JS or something

thheller19:03:06

note that I do no recommend this, it comes with more downsides than :js-provider :external

geraldodev19:03:10

You don't recommend :js-provider :import ?

thheller19:03:27

I don't recommend post processing the shadow-cljs output with a secondary build tool

geraldodev20:03:15

For development that is more than fine, but If we are bundling many npm dependencies and targetting production, to strip unused code there isn't another way than :js-provider :import and let another tool to be used. Is that right ? What would be the downsides ?

thheller20:03:34

I don't know what you are asking

thheller20:03:54

I don't know what these sizes represent. I don't know what you are trying to optimize

thheller20:03:21

did you create a build report and check what is actually in your build? https://shadow-cljs.github.io/docs/UsersGuide.html#build-report

geraldodev20:03:40

I didn't create a build-report. These sizes represent a bunch of @radix-ui components, that I'm using to test the configuration, and main.js is the entry point of :target :browser shadow-cljs app with just a component on the screen. What I'm trying to achieve is way to tree-shake the three-shakeable code that are bundled in lib.js. It seems to me that if I'm using :js-provider :external that's not possible, because I'm not feeding main.js to a external tool. What I understood from what you said, is that there is another option, :js-provider :import and I'm assuming that it does not generate a target/index.js, and I'll have just one artifact, main.js, and the external tool can process that code and hopefully tree-shake the javascript code.

thheller20:03:05

> It seems to me that if I'm using :js-provider :external that's not possible

thheller20:03:06

it is. it doesn't need to see main.js, everything it needs to know is contained in the external-index

thheller20:03:18

:external-index-format :esm being the essential part, without that no tree shaking can occur

thheller20:03:43

AND it only works for release builds

thheller20:03:59

as tree shaking in dev makes no sense

geraldodev20:03:29

I'm using :external-index-format :esm

geraldodev20:03:20

Please tell me , how It can effectively remove js code if it just see the imports in the index.js and do not have access to main.js which has the usage or not of the import ? I've noticed that If I put a library in package.json it only goes to target/index.js if it's imported in cljs code. So the shadow-cljs compiler is already computing what is being imported in target/index.js.

thheller20:03:13

literally just look at the :external-index file that is generated

thheller20:03:21

shadow-cljs knows what you used in the code, since it built that. so that is put into the external index.

geraldodev20:03:24

This one is very small target/index.js

import * as i0 from "react-refresh/runtime";
import * as i1 from "react";
import * as i2 from "react/jsx-runtime";
import * as i3 from "react-dom/client";
import * as i4 from "@ui/components/ui/label";
import * as i5 from "@ui/components/ui/input";
import * as i6 from "@tanstack/react-query";

const ALL = {};

globalThis.shadow$bridge = function(name) {
  const ret = ALL[name];
  if (ret == undefined) {
    throw new Error("Dependency: " + name + " not provided by external JS!");
  } else {
    return ret;
  }
};

ALL["react-refresh/runtime"] = i0;

ALL["react"] = i1;

ALL["react/jsx-runtime"] = i2;

ALL["react-dom/client"] = i3;

ALL["@ui/components/ui/label"] = i4;

ALL["@ui/components/ui/input"] = i5;

ALL["@tanstack/react-query"] = i6;

thheller20:03:37

that is not from a release build

geraldodev20:03:13

The output of a release build is different. Now that's make sense.

geraldodev20:03:58

Thank you for you patience and support 🙂

chico18:03:32

Hello! 👋 I'm facing an issue when tree shaking nested references for dependencies imported with :refer . If I use the dot notation (.-nested parent) or import it using the :as reference it seems to be working fine. Am I missing something or is this a known issue? (I created a tiny repo with the reproducible issue here: https://github.com/vloth/treeshaking-shadow-repro) [also let me know if you'd like me to re-ask this in a regular message and not in this thread to keep things separated]

thheller19:03:26

AppShell.Header is not valid. it just works by accident basically. and what "breaks" exactly? please include actual errors in repros. running stuff takes to long and most of the time I can answer by just seeing the error

chico19:03:39

sorry, I've added there! Using it together with reagent, I get an error from react telling me I tried to render undefined , which cannot be accepted as a valid component. I did some troubleshooting and it seems like the library itself doesn't change with these syntax changes, so it seems like the undefined is coming mostly from main.js. Almost as if the bridge function were not able to connect things as expected

thheller19:03:57

yes, so this is an externs issue

thheller19:03:25

AppShell.Header is not correctly detected as needing externs, and it ends up getting renamed

thheller19:03:39

has nothing to do with tree shaking or :js-provider :external

thheller19:03:28

basically it is looking for , which is undefined and thus fails rendering

thheller19:03:48

I'm unsure why these externs are missing though. I thought that was fixed

thheller19:03:07

but yeah basically just avoid dotted symbols

thheller19:03:33

(. AppShell -Header) or (.-Header AppShell) both should be ok

🎉 1
chico19:03:43

ahh gotcha! makes total sense, thank you very much! 🙏