Any chance we could reduce the bundle size in development?


#1

Even though my own code only contains tens of modules, shadow-cljs has too load a bunch of files from Closure Library and Clojure standard library. Most of code can be dead code eliminated during optimization:

=>> du -a *.js | sort -n -r
4224	cljs.core.js
1152	cljs.pprint.js
640	cljs.spec.alpha.js
640	cljs.core.async.js
256	shadow.object.js
256	shadow.dom.js
256	shadow.animate.js
256	cljs.tools.reader.js
256	cljs.spec.gen.alpha.js
224	goog.dom.dom.js
192	goog.base.js
152	goog.style.style.js
120	shadow.cljs.devtools.client.browser.js
120	goog.array.array.js
104	shadow.cljs.devtools.client.hud.js
104	goog.string.string.js
88	goog.uri.uri.js
88	goog.promise.promise.js
88	goog.net.xhrio.js
88	goog.iter.iter.js
80	goog.html.safehtml.js
80	cljs.tools.reader.reader_types.js
72	goog.uri.utils.js
72	goog.events.events.js
72	cljs.tools.reader.edn.js
64	shadow.cljs.devtools.client.env.js
64	goog.math.long.js
64	goog.i18n.bidi.js
64	cljs.core.async.impl.ioc_helpers.js
56	shadow.xhr.js
56	goog.debug.logger.js
56	cljs.tools.reader.impl.errors.js
56	cljs.core.async.impl.channels.js
48	shadow.cljs.devtools.client.console.js
48	respo.render.diff.js
48	goog.object.object.js
48	goog.math.integer.js
48	cljs.reader.js
40	goog.useragent.useragent.js
40	goog.result.resultutil.js
40	goog.labs.net.xhr.js
40	goog.html.safeurl.js
40	goog.html.safestyle.js
40	goog.dom.safe.js
40	goog.debug.debug.js
40	cljs.tools.reader.impl.utils.js
40	cljs.core.async.impl.buffers.js
32	goog.window.window.js
32	goog.structs.map.js
32	goog.math.rect.js
32	goog.math.math.js
32	goog.json.json.js
32	goog.html.trustedresourceurl.js
32	goog.html.safestylesheet.js
32	goog.functions.functions.js
32	goog.events.eventtarget.js
32	goog.dom.tagname.js
32	goog.dom.forms.js
32	goog.asserts.asserts.js
32	clojure.string.js
32	clojure.set.js
32	clojure.data.js
32	cljs.repl.js
32	cljs.core.async.impl.timers.js
32	cljs.core.async.impl.protocols.js
24	shadow.util.js
24	respo_ui.core.js
24	respo.util.format.js
24	respo.render.patch.js
24	respo.render.dom.js
24	respo.core.js
24	goog.timer.timer.js
24	goog.structs.structs.js
24	goog.math.coordinate.js
24	goog.math.box.js
24	goog.labs.useragent.browser.js
24	goog.html.uncheckedconversions.js
24	goog.html.safescript.js
24	goog.events.listenermap.js
24	goog.events.listenable.js
24	goog.events.eventtype.js
24	goog.events.browserevent.js
24	goog.dom.classlist.js
24	goog.dom.asserts.js
24	goog.disposable.disposable.js
24	goog.async.nexttick.js
24	cljs.tools.reader.impl.inspect.js
24	cljs.tools.reader.impl.commons.js
24	app.comp.container.js
16	respo_message.comp.message.js
16	respo.render.expand.js
16	respo.controller.resolve.js
16	respo.comp.inspect.js
16	recollect.patch.js
16	goog.useragent.product.js
16	goog.style.transition.js
16	goog.string.stringformat.js
16	goog.string.const.js
16	goog.result.simpleresult.js
16	goog.reflect.reflect.js
16	goog.promise.thenable.js
16	goog.net.xmlhttp.js
16	goog.math.size.js
16	goog.log.log.js
16	goog.labs.useragent.util.js
16	goog.labs.useragent.platform.js
16	goog.labs.useragent.engine.js
16	goog.html.legacyconversions.js
16	goog.events.event.js
16	goog.debug.logrecord.js
16	goog.debug.logbuffer.js
16	goog.debug.entrypointregistry.js
16	goog.async.run.js
16	cumulo_reel.comp.reel.js
16	clojure.walk.js
16	app.comp.profile.js
16	app.comp.navigation.js
16	app.comp.login.js
16	app.client.js
8	ws_edn.client.js
8	shadow.module.client.append.js
8	shadow.js.js
8	respo_ui.colors.js
8	respo_message.schema.js
8	respo_message.comp.messages.js
8	respo.util.list.js
8	respo.util.dom.js
8	respo.util.detect.js
8	respo.schema.op.js
8	respo.schema.js
8	respo.env.js
8	respo.cursor.js
8	respo.controller.client.js
8	respo.comp.space.js
8	recollect.util.js
8	recollect.schema.js
8	module$node_modules$shortid$lib$util$cluster_worker_id_browser.js
8	module$node_modules$shortid$lib$random$random_from_seed.js
8	module$node_modules$shortid$lib$random$random_byte_browser.js
8	module$node_modules$shortid$lib$is_valid.js
8	module$node_modules$shortid$lib$index.js
8	module$node_modules$shortid$lib$generate.js
8	module$node_modules$shortid$lib$build.js
8	module$node_modules$shortid$lib$alphabet.js
8	module$node_modules$shortid$index.js
8	module$node_modules$nanoid$format.js
8	hsl.core.js
8	goog.string.typedstring.js
8	goog.string.stringbuffer.js
8	goog.result.result_interface.js
8	goog.result.dependentresult.js
8	goog.promise.resolver.js
8	goog.net.xmlhttpfactory.js
8	goog.net.xhrlike.js
8	goog.net.wrapperxmlhttpfactory.js
8	goog.net.httpstatus.js
8	goog.net.eventtype.js
8	goog.net.errorcode.js
8	goog.math.irect.js
8	goog.json.hybrid.js
8	goog.fs.url.js
8	goog.events.listener.js
8	goog.events.eventid.js
8	goog.events.browserfeature.js
8	goog.dom.vendor.js
8	goog.dom.tags.js
8	goog.dom.nodetype.js
8	goog.dom.inputtype.js
8	goog.dom.htmlelement.js
8	goog.dom.browserfeature.js
8	goog.disposable.idisposable.js
8	goog.debug.errorcontext.js
8	goog.debug.error.js
8	goog.async.workqueue.js
8	goog.async.freelist.js
8	cumulo_util.core.js
8	cljs.user.js
8	cljs.core.async.impl.dispatch.js
8	app.style.js
8	app.schema.js
8	app.config.js
8	app.comp.workspace.js

Although it’s cool when I develop my code with localhost, when I develop with IP address, it brings problems(not figuring out why, but there are too many requests):

Besides, sometimes I need to run my code in another devices, like phones, pads, the files goes over the network and then it’s loaded. Given the amount of files, it’s less convenient and in some cases it’s scary.

Yeah :loader-mode eval can solve major parts of this problem by merging all the files into a single file, then eval it. But there is still some issues that I have to wait and load a large file or use tricks to catch the exceptions.

So I do want to ask if there’s still any chance that we can remove the code we will never use, I guess I will not use goog.style.transition.js after it loaded. Then we can reduce the bundle size even in development.


#2

Short answer: No.

DCE during development is simply not practical. The whole point of development mode is fast feedback which you simply cannot get if you try to analyze the code on every recompile to figure out what is “dead”. Also things that were “dead” at one point may not be later (eg. you add a new function that uses another function that was previously unused). Reconciling that at runtime would be a nightmare.

I also do not see the point in trying to micro optimize out small namespaces like the goog.style.transition you mention. It is used, otherwise it would not be loaded. Sure we can try and remove the code that uses it but overall it wouldn’t change much.

Don’t know what is happening with your webserver and why it fails to serve the files? Is this with the built-in shadow-cljs server or something else? I have never seen such issues.

The one thing that could be done to optimize download times is to allow caching, it can greatly improve overall performance if rarely changed files (eg. cljs.core) are cached. I would consider adding a flag that lets you disable the “no-cache” policy. I have personally been bitten by too many caching issues in the past so I wouldn’t recommend it but that doesn’t mean we can’t add it.

Another thing you can do is enable https which will make the built-in server switch to http/2 which can also greatly increase performance when many files are involved. You do however get better results overall with :loader-mode :eval + gzip.

The last thing you can always do is code-split and delay loading code you don’t actually use initially until you actually do. This is just as relevant during development as it is in production.

:loader-mode :eval + gzip has shown the best results in my tests and I have not come up with anything better so far. I’m all ears if you have ideas but DCE is not it.

Not sure what this means.


#3

I also do not see the point in trying to micro optimize out small namespaces like the goog.style.transition you mention. It is used, otherwise it would not be loaded.

I see so many files from goog.* so I think it contains all the files in Closure Library. Any means to tell if it’s really used?

Don’t know what is happening with your webserver and why it fails to serve the files? Is this with the built-in shadow-cljs server or something else? I have never seen such issues.

I was using the built-in server. The browser is Chrome canary. I debugging the page with http://<IP_ADDRESS>:7000. If I visit the page with http://localhost:7000, it’s all fine. I guess Chrome is banning some of the requests when there are too many.

The last thing you can always do is code-split and delay loading code you don’t actually use initially until you actually do. This is just as relevant during development as it is in production.

Not trying that since my own core base is relatively small compared to cljs.core and goog.*.

Another thing you can do is enable https which will make the built-in server switch to http/2 which can also greatly increase performance when many files are involved.

HTTP2 for files also means WSS for WebSockets. I have to think about that. It’s a solution but not very easy for me…

You do however get better results overall with :loader-mode :eval + gzip.

Agreed. It’s the best solution at current. 5M is large, but I can’t say it’s too slow over local network.

… use tricks to catch the exceptions.

When code is running in :eval mode, exceptions are caught, which means my breakpoints are not taking effects as usual. I can still turn on “Pause on caught exceptions”, just less easier to get the specific exception.


#4

shadow-cljs only adds files to builds that were actually required somewhere. Whether or not something from those files is actually used is difficult to tell but they definitely were required by something. For example the goog.style.transition ends up included because of

entry shadow.cljs.devtools.client.browser
requires -> shadow.cljs.devtools.client.hud
requires -> shadow.dom
requires -> goog.style.transition

(I’m trying to find a good visualization for this info so you can quickly tell why something was included in a build. A rough preview will probably be included in the next version).

I have never seen this issue even in test builds that had 3000+ included files. Do you have any plugins/extensions which may block those requests? Any configured proxy?

You mean exceptions thrown while loading the file?


#5

A rough preview will probably be included in the next version

Sounds cool.

Do you have any plugins/extensions which may block those requests? Any configured proxy?

I’m using a proxy but the requests to local networks should not using it. I should try with a cleaner environment next I see it.

You mean exceptions thrown while loading the file?

I think it happened during my interactions with the page, not during loading. (Not very certain.)


#6

I did some experiments and as of 2.7.22 enabled “soft” caching by default. This means that the browser is still forced to revalidate before using the cache but the server can reply with a “304 Not Modified” to allow skipping the actual download.

Depending on your network configuration and overall build composition this can outperform :loader-mode :eval. You may want to experiment a bit with this yourself since the results vary greatly. I still find :loader-mode :eval to have the best results in larger builds with lots of files (especially when accessing non-local servers).

Maybe it helps a little bit.


#7

Not sure if it’s useful but I recorded a video when I see the problem again. It’s fixed after I switched back to localhost:7000. https://youtu.be/ZEE6SHjL-7M


#8

Is that your local IP or something running in a container or so? The server should basically never take that long to answer a file request. I’d suspect something inbetween you and the server. Might be a firewall, proxy, chrome extension, etc, etc …


#9

Please try using a different webserver and see if it behaves the same. Any other webserver will do but should probably use a fast one (eg. nginx), otherwise it just might naturally throttle due to the webserver answering slowly.


#10

I think it’s not a problem in the webserver. In the video I also tried using curl to fetch the file and it worked.

I’m tried http-server from npm, it also triggered this problem at times.


#11

Tried several times today. Now I think it’s my proxy causing the problem.