Code bundle splitting of Elm programs

I created a script to split an Elm bundle into one ESM per program, each importing the common code from a single shared file (demo).

How does it work?

By post-processing the generated Elm bundle, see this explanation post.

Usage

# Compile multiple Elm programs into one bundle
elm make --optimize --output=examples.js src/Clock.elm src/TextField.elm src/Quotes.elm

# split the bundle
npx split-elm-bundle examples.js

And then you can import a program e.g. like this:

<!doctype html>
<main id="elm"></main>
<script type="module">
  import { Clock } from './examples.Clock.mjs';
  const app = Clock.init({ node: document.getElementById('elm') });
</script> 

Is the tool useful? It depends :person_shrugging:

Bundle splitting is very common in JS frontend framework, but so far Elm did not do it. The question pops up from time to time but usually the advice is not to worry about it.

In general, our bundle sizes are quite small

Since 2018, we usually get small bundles without the headache.
For instance the Elm implementation of TodoMVC weighs 9kb (if uglified), and the Elm realworld example weighs 29kb. For comparison the React runtime alone weighs 32kb (back in 2018).
From the list of maintained realworld examples, I looked at a few react versions and this one seems to be one of the smallest ones (in bundle size), and generates two files totaling 85kb (2.a9e8cf08.chunk.js 77kb and main.7edcaa81.chunk.js 9kb).
All numbers assume that the JS code was minified (e.g. with uglify-js or terser) and gzipped.

Works only for multiple programs that are compiled into one bundle

My approach splits per Elm program. So if you don’t have multiple programs that the user is likely to open, then this kind of splitting won’t help you.
If you could split your web app into a public part and into an admin area, I would guess that most times it is better to just split the two sections into two programs and not share code, because there will be little overlap where the logged-out user browses the public part (and then later also after logging in).

If you want to compile multiple Elm programs separately, the optimizations of the Elm compiler make it hard to share code, see this explanation by @supermario.

Likely usage scenarios

When building single-page-applications it could be useful because the tool could generate necessary glue code to switch between different programs and to also share state between the different programs.
It would be also nice to have e.g. one program for the header bar with menu links that can load multiple other programs per page.

I would like to see how a tool like elm-pages or elm-land that already do a lot of code generation, could generate all the ports and js glue code to seamlessly switch between different programs.\

Allowing them not to bundle all of the pages into one program, but load only what is needed, instead.

A nicer experience could also be if some routes in a single Elm program are loaded on first use, but this would need a different kind of code generation (e.g. create a type for loading the other ESM and then making it visible after information from a port).

End?

Thanks for your attention, I would be interested in your ideas how to use such a tool or in general feedback and comments.

And in case you missed the link at the very top, here’s the demo.

16 Likes

This is a very nice project here! Unfortunately, I don’t think it’s going to have any use in either elm-pages or elm-land for the reasons that you referenced in Mario’s gist

Both elm-pages and elm-land compile into a single frontend Elm single-page application. If they were to leverage this technique, then they would need to use a sort of component style of loading separate Elm apps (that is, sharing state between separate apps instead of within a single Elm application). In theory a tool could use something like this, but it wouldn’t allow for data to be sent back-and-forth without serializing it through ports, so that would be limiting (and it would also require either manually doing that serialization, or using a framework to build that glue code to wire up components for separate Elm apps).

So while this is a very neat technique, it will unfortunately not help with code splitting for these Elm single-page app frameworks, which is too bad because that is something that I would really love to be possible! Either way, thanks for sharing the post, and I hope that it proves useful in some other contexts!

This looks interesting to me.

The use case I can see for it is where we might mix Elm with other web technologies, and server side rendering and routing. So I might have the main routing happen on the backend, and it might serve up some pages using say Elixir liveview, but other routes might serve up a client rendered Elm application. Presumably the common code would be cached making each Elm view faster to load.

Never having used either of them properly, I was not sure how hard it would be to add a new mode that would e.g. to add a build flag to switch from generating code for nested TEA pages(?) to creating several programs instead.

Yes, I mostly saw it for a static site builder approach. You definitely need to pre-build the Elm programs and should be reasonably certain that users will visit more than one program.

The idea with elixir sounds nice, similar to how djelm could also use the tool, I think.

Great idea.

I think the use case with separate admin section is very important from security perspective. I’ve seen production Elm apps where the login page already contains the complete app code. One can just download the whole app without ever logging in and reverse engineer all compiled-in graphql queries, endpoints it uses for administration, and other details. Not to mention that sometimes it can be multiple megabytes of the whole app served just to show the login.

The same applies for the apps that have ‘member’ and ‘admin’ sections–it would indeed be nice to load the admin part on-demand from a link in the header, from an endpoint that can serve the feature code only if the logged in user has admin permissions. That way non-admin users save a lot of bandwidth, as usually admin functionality is bulkier than non-admin-oriented features. And again, minimizes the opportunity for reverse engineering.

As for keeping state on load of the new app section, maybe that’s something that needs to be done already at Elm code level? Some kind of init that is aware of other sections’ model shapes?

1 Like

Hi team!

Super interesting conversation and not sure if this is relevant but I’ll add anyway.

At the moment, djelm bundles Elm programs separately. So if you use two different Elm programs on a template, then you are actually getting two Elm run-times on the page. Obviously not ideal for folks that want to use lots of tiny Elm programs sprinkled across a server rendered page, but it is quite easy to share the runtime by compiling the programs for a given route together.

They are also dynamically loaded, so if you click a server rendered button that renders a djelm tag, then the hydration code responds to that and fetches the given Elm program. So in this scenario if your different Elm programs are popping in on user interactions, then you are fetching the Elm runtime, one program at a time, only when they are needed. Obviously this gets cached in the browser for subsequent ‘popping’.

The really cool thing about Django, is you can introspect it and find out that a server rendered template is using different Elm programs, and you could collect this information at run-time in a dev environment and (in some future djelm) suggest to the user “Hey, you should bundle these separate Elm programs together for a smaller asset on x template”. So that’s pretty cool, but it’s entirely possible that a handful of different small Elm programs on a page is not really a performance issue.

In regards to sharing state between separate Elm programs, I thought about this from pretty much the start and it’s totally doable. Say you had a ElmSelect.elm program in a server rendered form field, then at the bottom of the page after a bunch of server rendered content you had another Elm program that did something else, but you want to know when the ElmSelect.elm state changes, like when a selection is made.
Well they are two isolated programs, but because their init state and decoders are related to python flag declarations and generated with djelm, they are 100% serializable and thus could be subscribed to through some djelm instrumentation.

Now, this starts breaking down when an init state generated via djelm is converted by a user in to some other view optimized state that can’t be serialized. Or, the Elm program actually doesn’t need any Django values and creates it’s own state. Lot’s of edge cases.

Nonetheless, if the user uses the init state generated by djelm as their working state, then that can be shared with other programs!

This would all mean that the djelm hydration code would have to get A LOT smarter about handling port subscriptions between programs. I imagine djelm you could use a “signals” pattern to do so.

Anyway, I hope this was relevant and didn’t steer anything away from the initial conversation path! I am very excited however about all the different Elm flavored things popping up!

Also @marcw ,

This tool looks like something I absolutely want to use for djelm!!! Amazing job!

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.