gohugoio / hugo

The world’s fastest framework for building websites.
https://gohugo.io
Apache License 2.0
74.63k stars 7.45k forks source link

Support ESBuild Code splitting #7499

Open Vincent-Carrier opened 4 years ago

Vincent-Carrier commented 4 years ago

Currently, these are the only options supported by js.Build https://godoc.org/github.com/gohugoio/hugo/resources/resource_transformers/js#Options

However, ESBuild exposes all of these. There's a lot of good stuff in there, including control over the output format (presently defaulting to IIFE), code-splitting and sourcemap support. Code-splitting is especially useful since it prevents users from re-downloading the same code twice. Sourcemaps are another important one for debugging.

@remko Thanks for the new feature BTW, it's really great!

bep commented 4 years ago

Just a quick note that the above is not about just adding some more options -- which is the reason they're not there in the first place.

remko commented 4 years ago

The only thing that could be 'just adding some options' is inline sourcemaps (which would still leave one output file). However, this is only sensible in a conditional in development mode, so I left it out of the initial patch as well.

bep commented 4 years ago

I have been looking at the code spitting "thing" (which I assume will make this really interesting).

The challenge with this one is that it requires multiple entry points to be usable/interesting. But I'm not totally sure how to express this.

Thinking out loud:

{{ $entryPoints := resources.Match "**/index.js" }}
{{ $js := resources.Get "home/index.js" }}

{{ $js = $js | js.Build (dict "entryPoints" $entryPoints) }}

Would the above be clear enough? /cc @regisphilibert

Vincent-Carrier commented 4 years ago

I think I got a bit confused in regard to code-splitting. From what I now understand, Code-splitting is when you use async imports, e.g.

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

This is nice for web apps, but for blogs and documentation the use cases are more limited.

Chunking is the idea of having more than one file in your bundle output, so that you get better caching / less code duplication.

Code-splitting in ESBuild still seems experimental and limited to the ESM output format. Chunking is something you can do manually with re-exporting e.g.

// my-utils.js
export { range, take } from 'lodash-es'

becomes something like

// my-utils.bundle.js
// <big bundle of code>
export const range = ...
export const take = ...

However it's a bit unclear how one would make that work in the present context, when all your JS source files are in your assets folder. You need to tell ESBuild where to split your code, which if I understand correctly is what "entry point" alludes to (if that's the case, then I propose we use a different terminology because something like my-utils.bundle.js is clearly not an entry point into my program).

Since chunks / entry points are gonna be shared across your project, maybe it would make sense to declare them globally in config.toml.

regisphilibert commented 4 years ago

I'm fairly limited in the JS realm, so my input albeit naming and UX might not be too acute.

Vincent-Carrier commented 4 years ago

Thought about this some more:

You can mix both patterns together e.g.

// my-utils.js
export { range, take } from 'lodash-es'

and then in your consumer code

const { range, take } = await import('./my-utils')

The advantage here is that you only need to declare your entry points / chunks in your source files. The disadvantage is that you're using async/await for something that doesn't really need to be.

bep commented 4 years ago

@Vincent-Carrier Re. your last post, given that you have more than a few external dependencies (which I I see is not uncommon in the crazy world of JavaScript), I think you quickly will get a "management problem" doing this in Hugo (keeping track and building the chunks ...).

I don't think it's very uncommon, even for a documentation site, to have page or section specific JS code, and to be able to just import directly from the NPM dependencies without having to worry (too much) about the bundle size would be golden (to me).

But I think this problem needs to mature a little bit (both here and on the ESBuild side) before we attack it.

Vincent-Carrier commented 4 years ago

@bep Completely agree. My website only has two dependencies at the moment (lit-html and rough-notation), and so far I've been shipping them unbundled without any performance problems. The problem with the JavaScript ecosystem at the moment is that so many packages are either straight up impossible to use without a bundler or they hurt performance by deeply chaining imports. It's been a fun challenge to work around that constraint (kind of like the JavaScript equivalent of going vegan), but eventually there are times when a bundler is the only solution that makes sense.

@remko If ever you'd like to walk me through your implementation, I'd be really excited to try and build a proof of concept myself (I'm completely new to the Hugo codebase).

bep commented 4 years ago

@Vincent-Carrier I have split these "options tasks" into separate issues. The Format part is merged, for source maps, see #7504

earthboundkid commented 4 years ago

FWIW, my news site has run into a "chunking" issue already. I have two JS entrypoints: main.js (which is Babel'd for IE11) and enhancements.js (which requires a modern browser). Both of them end up importing metrics.js, which does some localStorage stuff to figure out when you were last on the site if ever. It's not the end of the world in my case, but in general, it would be better if metrics.js were only executed once, so you're not wasting time doing the same calculations twice. (Downloading once would also be nice, but not if means that two requests become three.)

Anyway, all this is to say that even for a "simple" site, JS dependencies can get hairy pretty quickly.