Hacker News new | ask | show | jobs
by franciscop 1206 days ago
So basically Deno has its own bundler that lets you not have a local build step and it gets bundled dynamically per-route as requested by users, right? This is very different from industry standards and possibly has many new concerns from devs, none of which are addressed in the article since it's treating the system as a perfect solution, which makes sense since it's a marketing page ("content marketing"). If it was a 3rd party article, I'd be interested in things like:

- How do you measure bundle size and make sure it remains small? (e.g. make sure that a PR doesn't accidentally import a massive dependency)

- How do you measure bundling speed/performance, does it add significant time to the first request? To subsequent requests? Is it linear, exponential, etc. with the number of LOC? Again like in the previous point, how do we make sure there are no regressions here?

- How does this work, well, with absolutely anything else? If I want my front-end in React? Vue? etc.

5 comments

They are not using their own bundler. They are using esbuild at runtime to generate bundles for individual islands when the process starts up. Then they store those files in memory in a Map. When the bundle files are requested, it just pulls the copy that was generated at runtime.

Here is a link to the source where esbuild is used.

https://github.com/denoland/fresh/blob/main/src/server/bundl...

I personally think it would be better to bundle at deployment time so that the bundles don't need to be regenerated each time a new process starts up or on demand when a request comes in for one of the bundle files.

It's like you combined the warmup phase of the JVM with the compilation phase, except you still have to wait for the JIT, too. I better not hear anyone complain about JVM warmup times again. :p
Man the things that pass for innovation in the node-adjacent space continues to blow my mind. It feels like hte horrors of /r/programmerhumor meets generic internet hype-beast cycles.
Conceptually, I don't see how this is much different from JIT compilers in the JVM, CLR, and similar runtimes. You don't hear so much from Java devs about how they can't* see the machine code their customers are running. They talk about cold-start performance, but accept that the first few requests will be slower in exchange for the productivity and eventually-high-performance.

Now that I think about it, this is how V8 works too, for JS code itself!

Why wouldn't this principle apply to bundling?

JITs exist because 1. certain compilation can't be done at compile time, because the code is dynamically synthesized at runtime from data only available at runtime; and 2. knowing how the code is already being used at runtime through an interpreter, can help optimize the compiled code ("profile-guided JIT.")

Bundling on first request has neither of these advantages: everything that is getting compiled at runtime could have been compiled at compile-time; and no information is yet available on how the code will be used.

The difference is that the JIT doesn't "fail". Builds and bundling can fail, and I wouldn't want to trust that all the machinery the builder/bundler depends on (especially if it needs to fetch things over the internet) are available and working properly.

Put another way, as long as the `java` command is present and working on the production machine, I can be pretty sure my service is going to run and work (aside from any bugs in my code, of course). With Deno, more moving parts on the production machine need to be working properly in order to ensure things work properly.

JIT compilers at least do the work incrementally, profile the code to provide the best (or several) versions of native code, etc. That is, they adapt to the particular invocation, doing stuff an AOT compiler cannot do (especially for dynamic languages like JS).

I wonder if running the bundler on startup, and throwing away the (identical) result of a previous invocation of the bundler, makes much sense. It at least could persist it optionally, like Python does with .pyc files.

What kind of innovation would you prefer? (removed snark)

Web code is made of duct-tape, nothing new.

Deno is a pretty big departure from Node in some respects as far as I can see.

I admit I haven't used it, but if it keeps only half of its promises to simplify frontend bundling and compilation/transpilation, I think it's innovative.

A poor craftsman blames their tools.
Because a good craftsman picked his tools and knows it’s his fault if they’re garbage.

This pattern of “thinking” pisses me right off. It’s an axiom about quality and avoiding deflection and it’s always used in a low-quality reply as a form of deflection.

I can’t recall the last time I heard someone use that phrase the way it’s meant to be used.

A good craftsman knows not to use bad tools.
> A good craftsman knows not to use bad tools.

In your opinion, what's so wrong in prefering to just run your JS/TS code without having to maintain a build/bundling step?

To me, Deno's approach is undoubtedly a killer feature with regards to the status quo of the whole nodejs ecosystem. Don't you agree?

Builders and bundlers fail sometimes. I don't want to introduce an extra point of failure in my production services.

Maybe this is nice for local development. But really it just feels like the tooling version of a "code smell". If people think bundling/building is too slow, then people should work on making that faster. Maybe that means people need to stop writing JS builders/bundlers in JS, and use a language like Rust that has better performance characteristics. I wouldn't consider that a failure; it's just an admission that we should use the right tool for each job.

Speaking of Rust, the Rust compiler is fairly slow, but my proposed solution wouldn't be "get rid of it and have it dynamically compile at runtime", it's "profile it and make it faster" (which people are doing!).

Eh... I don't really think this is a killer feature.

If you don't want to maintain a build step, use a framework that's configured it for you and avoid customizing it.

Lots of frameworks already do that, this is just Deno's implementation of the same thing.

There's STILL a build step, they're running esbuild in the background for you. You've just lost visibility and control, exactly the same as if you picked a framework that gives you a default webpack config.

If anything, I see esbuild as the real "killer feature" here, since it's just really fast. Fast enough to bundle at request time.

Alternately, you can just stick close to standards and not really worry about it.

I write plain CSS.

I use Web Components as my unit of isolation, generally sticking with the light dom.

I have a small state utility [1] that I wrote years ago and works great.

I do have a build step before deployment, but I use vite during development so I have zero "make a change, wait, test, rinse, repeat" downtime. When it's time to deploy, vite build does the trick nicely.

I don't use frameworks. I don't use JSX. I don't use typescript, for types I use jsdoc in vscode which gives me 90% of the benefits of TS without the downsides.

My pages are light, fast and easy to maintain. I don't have to deal with painful build steps, or framework churn.

Debugging is simple. No multiple layers of transforms and sourcemaps, WYSIWYG.

I'm pretty passionate about the "keep it simple" philosophy.

I chose to innovate in the problem domain, not the technical one.

Anecdotally, I had a new developer join my team and he was initially very confused. He said "it's just so strange using this tech stack. You make a change, and you see it..."

I didn't know whether to laugh or cry.

[1] https://www.npmjs.com/package/applicationstate

My comment was more about the saying of the parents comment then specific to deno.

What I mean is that a good craftsmen doesn't complain about bad tool because they choose to use good tools (or more precise appropriate tools for the job) not because they ad-hoc easily negate any drawbacks of bad tools(1). And if they use bad tools anyway they do so intentionally or because there is not other choice and in turn don't complain because it's pointless to do so.

So the saying in the op comment is IMHO misleading at best deceptive at worst. Furthermore it doesn't advance any discussion, only side track it.

I'm sure the deno specific workflow from the article is a grate tool for a lot of use-case. I'm also sure there are use cases where it will fall apart.

(1): Depending on what you do creating a decent result without good tools might literally be impossible no matter how good your skill is. Or it might not make too much of a difference and can be compensated by skill, it's all context dependent, like most things in live.

The web is not a bad tool.
It sounds like (it is a little vague) there is no bundling at all, the only thing Deno does magically at request time is transpiling TypeScript/JSX to browser-compatible JS. Beyond that I think the idea is it relies on native ES module imports (and import-maps), both of which are browser standards
both of which are browser standards

ES modules have great support but import maps don't. Your website won't work on iPhones if you launch with them today. They're close though. Give it a month and it should work.

Based on https://www.digitalocean.com/community/tutorials/how-to-dyna..., aren't import maps a massive step back in the world of tree shaking? It would seem every export needs a dedicated file for it or else you get the whole world when you try to make a single request. (And better hope that single file doesn't import any other ones!)
I might be missing something, but I'm not seeing how import maps are related to tree-shaking (of individual declarations within a module)

Importing ES modules directly instead of bundling will probably mean you ultimately load more code, yeah, which is one reason bundling hasn't gone away. Though you get other benefits in exchange- more granular caching, free and very granular "bundle splitting" (every module is "split" and can be downloaded lazily or in parallel), on top of the simplified workflow.

Will be interesting to see how popular each approach ends up being, with ES modules getting more attention lately! I think at this point people will only use something if a framework they like uses it, for better or worse, so I'm glad to see a framework that's representing this alternate way of doing things

In the example on that blog post, you want lodash's "startCase" function.

In typical bundling with tree shaking, the developer would download all of lodash and the bundler would identify that only the "startCase" function (and it's deps) are referenced, it would smoosh them all together into one file along with the rest of your code, you put that file on a CDN, and you're done. The client can access everything they need with two requests: index.html, then app.js

With import maps, first loadash needs to be sure to split all their exports into individual files (they've done this for a while now as they predate tree shaking, but that's besides the point), then the client requests the website's JS, which tells it to go request https://unpkg.com/lodash-es@4.17.21/startCase.js, which then needs to make requests to https://unpkg.com/lodash-es@4.17.21/_createCompounder.js and https://unpkg.com/lodash-es@4.17.21/upperFirst.js. Then, _createCompounder.js needs to make a request to _arrayReduce.js (thankfully dependency free), deburr.js (depends on a ton of things), and you see where this is going.

All said and done, the client ends up making 32 (!!!) separate requests to the lodash CDN for that single function. And yes, this does parallelize somewhat, but its still 8 distinct "depths" of sequential request bottleneck as the browser can't get the dependencies for a file until it gets that file, and there are 8 levels of dependencies for that single function. On my home network this is 250ms just to load a single function, and when I simulate "Slow 3G" it's nearly 20 seconds, just for that one function! It's truly mind-blowing. Also, as lodash doesn't minify their code for some reason, the bulk of the bandwidth is in comments/etc and I download 30kB of useless crap for that single function.

When I instead use plain old bundling, the client makes a single request to index.html, which in turn makes a single request to app.js. The bundler does all the work of making app.js include the 10kb it actually needs (in 11ms no less), and even the "Slow 3G" client gets their website in 4 seconds.

All in all these "import maps" seem like a massive step back in every way that matters to the client, just to save the developer the 11ms it takes to run esbuild (which comes fully batteries-included, btw. The rollup days of "you get a plugin! you get a plugin!" are over).

Edit: this all sidesteps tree shaking as lodash is built to not need it, but you can see how if a library had multiple functions per file and especially if they had differing dependencies, the request chain would look much much worse.

Right okay, so, let's get our terminology straight real quick: an "import map" is just a piece of configuration that says, "when someone imports from module name X, load it from Y". This would for example let you `import { ... } from 'lodash'` and have it load from `https://unpkg.com/lodash-es@4.17.21`, or whatever else. That's all it does. Everything you're describing above is just about regular "ES module" behavior.

With that out of the way: yes, the lodash case would be pretty egregious if you imported the whole library in one. And most libraries imported this way will not be totally optimal: you'll probably load some code you don't need. But I think lodash is a pretty dramatic outlier; not only is it gigantic, it's exceptionally modular. Compare it to something like React, which is not small, but is nearly a monolith. The same I assume goes for Vue, etc, as well as other kinds of big libraries like GraphQL clients, third-party SDKs, etc. The percentage of code loaded that didn't need to be is, I would guess, usually much much smaller than it is if you're using a single function from lodash

I would add a couple more things:

- Minification is definitely a loss in the naive case, however, that should be easy for a CDN to implement (I think several already do it). I wouldn't be surprised if Deno/Fresh do this automagically too.

- HTTP/2 is optimized to make lots of parallel requests over a single TCP connection, which could conceivably mean a slightly larger total amount of code might load faster as separate modules than a single large bundle would. Of course like you said "depth" is still a limiting factor.

- For extreme cases, dynamic import() is an option in the native ES module system, and can be used to strategically defer module loading

So I don't think it's all that bad, even though like I said above there are tradeoffs. And I'll be curious to see where the industry goes.

PS: It would be good to have the option to bundle with Deno, though. One thing I would be excited to see, personally, is a Deno-ready bundler. One of the main limitations of using Deno right now, if you've got a front-end, is the lack of front-end tooling. You could install Node separately just for tools... but then that's a whole other system dependency, set of concerns, etc. I'd like to be able to do:

  deno run https://deno.land/x/bundler/cli.ts
And have that Just Work™. Maybe it uses WASM modules for speed.

...or maybe this could even be a first-class feature of the `deno` CLI

Fair. I wonder if Fresh includes some kind of polyfill for those (or maybe the transpiler factors in which browser the current request is being made from?)
That is if you use import maps on the client-side. It wouldn't matter to the client if you are using it in your server-side JS/TS.
I believe Deno is doing this, though (could be wrong)
> Transpiling TypeScript/JSX to browser-compatible JS.

Which is another way to say it's a form of compilation..... [0]

[0]: https://en.wikipedia.org/wiki/Source-to-source_compiler

Yes, but it's distinct from bundling, which is what most of the GP's concerns were around.
Being a bit hand-wavey, Deno's approach sounds like full-stack JS converging to PHP-like patterns, which wouldn't be a bad thing.
Maybe this is before your time, but there have been runtime minifiers/“builders” in the past. I remember them in the PHP days.

It’s not a new idea.

> How does this work, well, with absolutely anything else? If I want my front-end in React? Vue? etc.

It doesn't. Fresh uses Preact and that's it.

From the article: "Fresh renders each page on the fly and sends only HTML"

So the bundle size is zero.

Fresh supports islands, so it also does send JavaScript for the interactive bits. If you have a react (default is preact) island then that'll be bundled and sent down.
> unless an Island is involved, then only the needed amount of JavaScript will be sent as well

It's not just HTML but also JS that gets sent.