169 comments

[ 3.2 ms ] story [ 350 ms ] thread
Woah, importmap is a game changer! I didn't know about it until reading this, thanks!
Not yet available on Safari, but still cool!

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sc...

https://github.com/guybedford/es-module-shims has a polyfill.

(But it is fairly large: 53KB raw, 15KB gzipped, 32KB minified, 11KB minified+gzipped. It’s providing a lot of likely-unnecessary functionality. I’d prefer a stripped-down and reworked polyfill that can also be lazily-loaded, controlled by a snippet of at most a few hundred bytes that you can drop into the document, only loading the polyfill in the uncommon case that it’s needed—like how five years ago as part of modernising some of the code of Fastmail’s webmail, I had it fetch and execute core-js before loading the rest iff !Object.values (choosing that as a convenient baseline—see https://www.fastmail.com/blog/using-the-modern-web-platform/... for more details), so that the cost to new browsers of supporting old browsers was a single trivial branch, and maybe fifty bytes in added payload. I dislike polyfills slowing down browsers that don’t need them, by adding undue weight or extra requests.)

There's also https://github.com/systemjs/systemjs if you want more of a ponyfill approach. FWIW bundlers also don't use the browser's functionality to load modules...

It's a bit late to figure out, but I see caveats with dynamic imports in es-module-shims but not SystemJS.

I also sometimes enjoy this approach of starting from absolutely nothing.

Instead of taking the path of starting with DOM manipulation and then going to a framework as necessary, I've kept really trying to make raw web components work, but kept finding that I wanted just a little bit more.

I managed to get the more I wanted -- sensible template interpolation with event binding -- boiled down to a tag function in 481 bytes / 12 lines of (dense) source code, which I feel like is small enough that you can copy/paste it around and not feel to bad about it. It's here if anyone cares to look: https://github.com/dchester/yhtml

An even simpler bit of sugar over the DOM that I embed whenever I need just a little bit of JS:

  function $E(t,p,c) {
      const el=document.createElement(t)
      Object.assign(el,p)
      el.append(...c)
      return el
  }
Usage:

    const button = $E('button', {onclick: () => alert('click!')}, [
        $E('img', {src: '/assets/icon-lightbulb.png'}, []),
        'Ding!',
    ])
(With thanks to 'goranmoomin: https://news.ycombinator.com/item?id=23590750)
Oh, didn’t expect a reference to me at all, was thinking that the function was very similar to mine :)

That function is now in my must-haves in my new Django side projects; I usually overuse them before I finally move to a JSX-based UI library. It’s great for simple DOM manipulation… and for me it seems to create a semi-simple migration path in the case the code gets complex.

Wow! What a coincidence! I literally have something extremely similar which I called `$e`. I think one crucial thing you did not mention is that this conforms to the signature of jsxFactory, meaning if you run some kind of transpiler that supports jsx, you can literally do:

parent.append(<p>Hello</p>);

(Mine has slightly more functionalities, such as allowing passing in styles as a object, auto concatenate array of strings for class names, etc.)

Joining this thread to say that I, too, have written a very similar function and also use jsxFactory to have JSX support in personal projects. I find that using it along with an extremely simple implementation of a kind of state listener[0] produces something really nice for small projects.

It's a bit like a jquery for the '20s.

[0] https://github.com/curlywurlycraig/vdom-util/blob/master/src...

well sure, client side reliable ES means we can drop all this compiled and packed stuff

but bold move cotton

nice to see there is a polyfill

I’ll think about it for my next side project, but I’ll probably just do something I can add to my React + Node portfolio on github because recruiters and employers won’t know about this until 2027

The problem with writing a shell script to fetch and download NPM packages is that it won't do so in parallel. I'm sure that's fine if you only depend on one or two packages, but dependency explosion is practically a feature of the NPM ecosystem, and that'll stretch the script runtime. Not to mention fetching packages that you already downloaded, and no compatibility with outside tooling like Dependabot for automatic updates.
Not only that, properly managing dependencies would become a huge problem.
A different way to think about this is that most of the dependency hell relates to dependencies of your build tools. Not to dependencies of your actual application. And then of course some of those tools might be node.js tools and others might be things written in rust, go, or other languages. So, you need multiple package managers to deal with all of that.

A neat way to organize that mess is to just use docker for tools and use maybe a simple Makefile to call those tools. Build your docker container once, push it to a docker registry and use it many times. Also nice for CI/CD.

I did that for my website which is a simple static site that I generate with some bash scripts, pandoc, and other tools from markdown files. The scripts run in Docker. I don't use node.js for this but it doesn't matter. It's just another set of tools. The only tools I need on a laptop are docker and make to build this website.

The issue with npm is that it pretends that everything happens inside of npm/node.js. Just npm this or that. It's a package manager. It's a build tool. And it's a way to run tools that you install via the package manager. And then almost as an afterthought there are a few run-time dependencies that it minifies into your actual application as part of some convoluted bundling toolchain (typically installed via npm).

The actual size of those run-time dependencies is surprisingly small for most applications. And of course with modern web browsers and html 2 & 3, loading them as one blob of minified js is not necessarily even the best way to pull in dependencies anyway. You can load a lot of this stuff asynchronously these days.

What about the production build? I guess Importmap paths at least would need adjustment.

Not strong on the frontend that's why genuinely curious.

It warms my heart to see frontend web dev maturing like this, it really does. Kudos to the author for putting this out there, that shell script is nifty.

One trick I've used is to `npm install` my dependencies as normal but bundle them with esbuild, while also generating a typescript definition file. This gives me one file I can import in vanilla JS but also lets me check my JS with typescript.

Me too, I left Web development when node and npm were learning to crawl, given how things went in WinRT and Android world, I ended up refocusing on Web.

So when I got back I couldn't believe how bad the experience became versus SSR frameworks, and gladly focused on backend and devops stuff instead.

It is refreshing to see this starting to happen.

> So when I got back I couldn't believe how bad the experience became

Like frogs in a slowly boiling pot, most full time js devs have no idea the shit they put up with. IMHO modern js frameworks suffer from complexity rot; they are so over engineered it is a sad joke.

I think Vue.js is an excellent choice for this. They recommend bundlers and the default import doesn't come with the template compiler but the template compiler is fast enough to run when loading the page, and the vue template syntax looks clean inside a template string IMO, especially if you take care to minimize logic in templates.
view really deserves as serious a crack that react got, imo it's just better made. and vite is so good I think it's earned it.
I just don’t get the whole adding another DSL instead of using JSX or something.
JSX suffers from the same problem that early PHP did. Because you have the full power of JS at your hands, you need some very good linting rules if you don't want to end up with a mess combining presentation and functionality. By having a hard split between the two you're forced to build declarative templates.

Most React projects I've jumped into are really hard to understand regarding which template elements come from which pieces of code (partly due to JSX, partly due to design patterns which seem counter-productive). I've never had this issue with a Vue project.

JSX is quite a bit better than early PHP, but yeah, it has the same issue. I like both JSX and Vue templates, though (I prefer JSX with Vue or Preact).

I think JSX and Vue templates both are discovered, not invented, and have staying power because of that. JSX is neat because it is a really simple hybrid of two languages and is fully composable. Vue templates are neat because unlike so many other HTML templating languages, the templating is inside (X)HTML attributes, and they don't go too nuts with them like Angular did. It took time for Vue to support pretty much everything, but now it does. The nested <template> tag can be used to accomplish the same things as JSX fragments. The IDE tooling adds syntax highlighting and autocomplete to the JavaScript expressions. Even though it's HTML5, Vue went ahead and supported XML-style self-closing tags.

This is a balanced viewpoint, thank you for sharing! I probably have been burned a few too many times with legacy applications without good separation of concerns in the rendering layer to ever really prefer JSX to a strongly-separated templating language, but in the right hands they can definitely be well-written and -composed.

In a way JSX feels like GOTO statements.

I appreciate the principled avoidance of over-tooling and dependencies. importmap is neat!

edit: In the same vein; cross-linking to "Writing JavaScript without a build system (jvns.ca)" https://news.ycombinator.com/item?id=34825676

Yeah, two really similar posts in a short time
This has been happening on HN a lot nowadays.

Tons of instances where there there are two closely related posts on the front page at the same time. Wonder if one post is inspired by the other, or they are usually independent.

Doesn't work in Safari!
I am developing a media-heavy web app, and I was surprised at volume of features that work in every other browser but not Safari. Or features that behave just differently enough in Safari to require code re-writes. In this regard, targeting Safari feels a lot like targeting IE 6 back in the early 2000's.
is this the right place to shill a similar thing? (I use preact without bundlers for a while; also fast enough typescript without bundlers)

UPD:

- Preact template: https://github.com/wizzard0/naked-preact

- node/browser TypeScript loader: https://github.com/wizzard0/t348-loader

Thanks for sharing! :D
k k updated the comment) preact is bundled but intentionally non-minified so it's easier to debug. ts incoming, gonna clean it up a bit and write a README

UPD you recognize a software developer a mile away by his awesome estimation skills >_> published the typescript loader too!

(comment deleted)
Rails 7 use importmap as default!
What’s the benefit of the custom download-package script over npm install? Why would you want to roll a custom solution here?
Didn't get the idea as well. The end part of the post is like a seed for a future custom package manager or something
NPM will automatically install dependencies and run post-install scripts. Those are normally a convenience, but I can understand why someone would want to skip that.
Still. I'm all for avoiding npm when I can get away with it, but for TFA's case I would have just used the `--ignore-scripts` flag.
As the project becomes more than a simple example and requires more third-party libraries, this is just unmaintainable.
I prefer to use vite - you get handling of imports without needing a separate bundler step, but you get hot reload as well. I find it is a nicer experience, YMMV but it is good to have alternative options:

  FROM node:19.1.0
  WORKDIR /project/vite_project
  RUN npm init -y
  RUN npm install react react-dom
  RUN npm install -g esbuild
  RUN npm install vite
  EXPOSE 8081
  CMD ["npm","run","dev"]
The react install isn't normally there if I start light - but it shows that the path to throwing in a framework is smooth. Typically combined with:

  #!/usr/bin/env bash
  docker build -t vite_play -f Dockerfile.vite emp/
Obviously there is something to a bundler step happening in the background, but it is fast enough (and implicit) so it doesn't get in the way of rapid prototyping.
I agree, while it’s nice to go back to basics, I am more concerned with the end result, and vite works really nicely to achieve this. Any real project is going to use CI at some point so what’s the issue with having a build step in your pipeline?
To go quite a bit off topic.

The author says that he starts with a single html file and splits it or incrementally adds stuff when needed.

That course of action has proven to be a really bad idea on every non trivial web project I worked on.

Mostly for teamwork and maintainability reasons.

E.g there is no clear project structure, the next dev will not understand stuff and do things differently. And welcome to the chaotic legacy code hell. It's like Php all over but in Js.

I usually work for customers who to some extend now what they want. I choose an appropriate tech stack for that use case. The team can use that stacks conventions to develop the project. There is no need to slowly grow, everything can be done in parallel and due to the conventions stuff sticks together in the end.

This sort of work flow has a tendency to just pile incredible amounts of code in files thousands of lines long. That is going to be unmaintainable in no time.

Everyone claims they don't do that but they all lie (yes you too).

Also everyone has a chance to end up maintaining the resulting amorphous blob (could be you again).

This lacks tooling like hot reload, spell checkers, linting and so on. You will need that anyway so why don't you start with it?

Also you need tooling for deployment like config files. Tests, Ci and what not...

Lack of documentation. Since this follows your personal style your colleague needs to follow. A framework provides documentation for that. Working like this, the docs need to be written by the devs. Although they may claim otherwise they usually don't.

As someone who does this too: it depends. If you take time out every now and then to completely refactor your code base it can actually be surprisingly effective. I've done exactly that on my last project and I'm pretty happy with the end result, you can have a look for yourself:

https://gitlab.com/jmattheij/pianojacq/-/tree/master/js

This project will likely never be finished, there are always nice new things to add or requests from people, there is no commercial pressure because it is a hobby project and I don't have a boss to answer to. And even if such refactoring operations take me two weeks or more (this one I did while I was mostly just working on a laptop without access to a keyboard so it was sometimes tricky to ensure that nothing broke) in the end it is worth it to me because I am also paying the price for maintaining the code and if it is messy then I would stop working on it. Project dependencies are the absolute minimum that I could get away with.

The project moves forward in fits and starts, sometimes I work on it for weeks on end and sometimes it is dormant for months. In a commercial setting or in a much larger team I don't think this approach would work.

I looked through your code a bit, and there are a couple of things I'd flag in a code review (e.g. use of non-local variables). Ironically these are things that are a lot harder to do, and easier to spot, if proper modules are used.
I'm sure you would, and if you looked at earlier versions of that same code you'd see mountains more of those.

I'd love for everything to be perfectly pure though and side effect free. The pressure is always on to improve it further. As for code review, feel free and open tickets for whatever you spot and hopefully one day I'll get around to it. Better yet: submit pull requests.

> If you take time out every now and then to completely refactor your code base it can actually be surprisingly effective.

So much this!

Current project I'm on, I own the entire front end for a major modernisation (AKA rewrite) of a legacy application and we are working in 4 week "sprints". I'm giving myself two days every month just to refactor the code I wrote that month.

How you plan to structure a project never works out, you always find better ways as you go and as the objective and priorities change (they always do).

On another tangent, before pivoting into software development I used to be a mechanical/industrial engineer. The parallels between coding and CAD are enormous. With CAD you also need to be spending 10% of your time "refactoring" your model. It's almost exactly the same from a maintainability perspective and leaving the model with good hygiene for the next person to work on.

> How you plan to structure a project never works out, you always find better ways as you go and as the objective and priorities change (they always do).

I always joke I write everything three times. The first time to get a feel for the space, the second time because I think I now understand it and then again when I finally really understand it. Most of the times these three look absolutely nothing like each other.

I think that even extends over longer time periods to projects as a whole and an evolving team. Sticking to my current project which has a nearly 20 year legacy and looks nothing like what it was when it started:

V1: Perl CGI, HTML forms, very little js

V2: Perl CGI, JQuery, Ajax (done badly with js code generated by Perl code)

V3: Perl backend rest API (will be "public"). TypeScript+Vue front end.

Each time a transition has happened as the application features have outgrown the architecture, and more has bean learnt about how people use the product and therefore what it needs to do.

In this case it has now grown the the point that Node and build tools are required.

Neat, is this a public project. You make me curious what it is that you are hacking away at!
Avoiding Googleable backlinks, if you search for "integrated antibody sequence and structure tool" you will find it.

I'm the first "product" person without a bioinformatics background to work on it. The new version isn't out, but is a significant rewrite with an aim to massively extend its functionality in future. (Current "public" version isn't even the current commercial version, and very 90s in style!)

Backend is a very large Perl codebase implementing a significant amount of algorithms from academic research.

It's been awesome to work on, my ideal sort of project where I can bring a technical product focus and learn about interesting technology and science at the same time.

And super useful too. Wow. Thank you, I will definitely have a look.
You are optimizing for entirely different use cases.

There are a whole bunch of devs who work alone or in (very) small teams. For this type of work it’s really more of a hindrance to have an imposed structure and tooling.

We want to get to your goals as efficiently as possible. Minimal abstractions, guidelines, tooling, indirection, magic, surprises and general overhead are in order. We don’t want to struggle with questions like “how to do this in X”. We already know how to do it, so we just do it.

As time goes on and LOC get merged we find ways to add sensible structure and compress our code.

(comment deleted)
There's another comment that talks about medium to large project, and it's the same thing, it's a different use case. I don't care about medium or large projects, I need to add a quick semi-dynamic element to an incredible basic web app.

In my case, setting up a React or Angular project is going to be more work than just writing the whole thing in vanilla JavaScript. This has the added benefit that I actually understand what's going on.

The modern JavaScript frameworks are great for large projects, but it feels like learning Django in order to make an API that goes "pong" when you do a get request. I get why this happens. Vue looked great 8 years ago, small, simple and easy to get started with. Then it grew to support more and more use cases, and larger projects and now we need a new tiny framework. Or we can accept that JavaScript has come a long way and that many of us might not need a framework.

No, proper structure and tooling (especially for HMR) are important productivity boosters for solo developers as well. Single file scales to about 1k LoC, it’s only viable for toys and tiny projects; beyond 2k-3k LoC you’ll hate yourself every time you need to change something (been there). And ad hoc shell script in place of npm/yarn/whatever proper package manager, which is way easier than said script? I hope that’s show business and not actually used.

Btw, this direct module import approach means you’ll be shipping a lot of unused code to users.

Edit: If you only need a tiny amount of progressive enhancement on top of static HTML: forget about this import map and ad hoc npm script business, use petite-vue (single script, 6KB, add a script tag like good old jQuery).

> There are a whole bunch of devs who work alone or in (very) small teams.

And you still need to reimplement a bunch of stuff if you don't use oss packages. And the next maintainer will curse your family for generations because of your own custom framework.

Imo being "smart" like this is reinventing the wheel and doesn't give you any productivity gains.
It's not reinventing anything. It's just avoiding complexity until necessary.
Writing custom shell scripts to avoid complexity usually achieves the opposite result. I can Google npm errors, but not why a custom shell script fails.

Second, anyone reading the repo is now expected to understand not 1 but 2 languages.

Finally, the shell script performs basic tasks that npm does out of the box.

Because of above reasons I think this solution is needlessly complex.

There is no clear project structure if you don't write down what the expected project structure is. Which you really need even with a framework, because the frameworks only provide really rough high level structure.

While I loathe files thousands of lines long, I also loathe the endless chasing of things from file to file that tends to be the result of applying big frameworks to tiny projects.

Really? It has been well over a decade since I cared about file length or number of files. Any number of code editors has search, multi-file search, and "go to definition". I've got one personal project that is a three.js character creator, where my js is one file of somewhere north of 200K lines. Who cares? I sure don't. If another were to join the project, sure, I'd break it up into smaller files so others can work on separate bits - but that's the only value of separate files anymore. Nobody prints code anymore, and if they do it is code fragments and not one's entire program.
I find it a lot easier to reason about logical chunks of code that are clearly divided. I rarely want to see just a specific function, but a cohesive unit that can be read in sequence and make sense. You can do that in a single file too, but in my experience, large files tends to lead to code being spread out without a narrative determining order because people (myself included) are rarely disciplined enough. Files act as a convenient clear grouping to me. On the other extreme, when people insist on splitting everything up into the smallest little unit, it drives me entirely nuts for the same reason. I want to be able to read through the code without jumping back and forth all the time.

But when it's just you it's all down to preference, so do what works for you.

I get where you're coming from, but I have on occasion wanted a fairly simple one-trick-pony sort of SPA. Think a GNU userland tool, not Word.

Thinking outside the node-vue-kitchen sink sort of development box is a good idea. Maybe you don't have a grand idea, maybe you just have a kinda neat idea. Punch that thing out fast and light. You can always grow it bigger, but maybe you don't need to.

You can use it on your sites that will probably be used by 5 people. Good luck.
On the same topic, I wrote some articles a while ago. Here is my own approach for making a VanillaJS SPA without using any bundlers or frameworks https://rishav-sharan.com/#/making-a-spa-in-vanilla-js

Note that this blog right now converts markdown posts into html at runtime, which is definitely not an efficient way of doing things. But considering that I have just a handful of people landing there, I really haven't tried to update the approach.

Just wanted to say I enjoyed reading this. Thanks for sharing.
Nice work, and nice writing too!
Clever and minimal approach!

I wonder whether you could turn this into a static renderer by skipping the `after_render` as a separate step that only happens in the browser. You could even rename it `hydrate` if you want to use fancy modern JS lingo. The benefit would be to show content on initial page load without JS enabled as well as letting crawlers index your site (some still don't run JS).

A common problem I run into is wrt routing, and I see you've built a hash-based routing system. Are you worried about overriding the expected functionality of the URL hash?
I really enjoyed reading that, it's always a pleasure to get a window into the thought process of other developers as they evolve their ideas. Just curious, did you consider using event bubbling so you can declare event handlers right away rather than doing them in the after render section? This is currently the approach I am taking, and I'm just wondering if there are some pros and cons here.
While Safari doesn’t support importmap, it’s possible (and not too hard) to use them as a progressive enhancement so that it’ll still work, and safari users will get the benefit with 16.4. I did a writeup: https://qubyte.codes/blog/progressively-enhanced-caching-of-...
Firefox only started supporting import maps in v108 which was released 3 months ago. Previously you would need to use an experimental flag to enable it.
I like Astro for this reason. It feels light and modular compared to the other frameworks.
Have you tried the same with remote imports to save the manual download step?
If you use normal includes instead of imports, and put an object (or better a closure) on the top as a namespace, your SPA will work with any browsers, and you don't force your users to throw out their phones and laptops every 2-5 years.
You could also just use a polyfill (es-module-shims) to add support for older browsers.
(comment deleted)
Maybe you are right, and modules (even this fancy new module syntax) are not worth to fight against anymore.

I don't really trust polyfill tho.

could you make a code example?
I was thinking of including with

    <script src="mod.js"> </script>
just how you include (or included in the past) jquery or mathjax. I manage my namespaces like

    function Zebras() {

      let l;  //local
      let e;  //has setter and getter
 
      gete =  () => {return e}
      sete = (x) => {e = x}

      function lfunc() {console.log("local function")}
      function efunc() {console.log("exported function")}
 
      // exports :
      this.efunc = efunc
      this.gete  = gete
      this.sete  = sete
   }
and import it

    <script src="mod.js"> </script>

    <script> 
 
      zr = new Zebras()

      zr.sete(4)
      console.log( zr.gete() ) // 4

      zr.efunc()
      // zr.lfunc() <- not working
 
    </script>
You can export objects or functions trivially. I don't think you can expose primitives this way.

I think this method is working since ~20 years, and will work 20 years from now, when you can't even get your bundlers to run. But then if you have to change it frequently, probably ES modules are easier to automate, test, and handle with bundlers.

I'm a fan of this actually. Thanks for introducing me to importmap type!
Import maps strike me as a huge win for making prototypes, or things where you control what exactly you're importing (eg from a private registry instead of NPM), but throwing out the bundler and more importantly the tree-shaking and minification steps of bundling, will result in a massive about of unused code being delivered to the user. You'll be relying on maintainers putting minified, unbloated assets in their packages and ... well ... NPM is gonna NPM.
I think the issue might be the name "tree shaking". People from outside of the JavaScript just don't understand what it means, and how important it is. Maybe we should have just called it "dead code elimination". (I think there is some subtle difference but my memory is failing me).
IIRC tree shaking is a form of dead code elimination. Something like: dead code elimination is the what and tree shaking is the how.
> you'll be relying on maintainers ..

Our team (at Fortune 500 co) are pretty agile and forward looking (compared to corporate IT) but we're still not allowed to use anything from NPL-like repos without permission, and even then it has to get a code review and be pulled into our local repo. Personally, I agree with the CTO's belief that going from NPM directly to production is sort of insane.

This is where ES Modules come into their own and why importmaps are very much an ESM thing, predominantly. ESM was built with "natural" tree shaking in mind in a browser context. Only URLs that are actually imported in a module are loaded. (importmaps are not prefetch maps.) Modules themselves are loaded in a such a way that unused exports in them may be lazy-JITted "weak references" and easily garbage collected (a form of tree-shaking).

You get better "natural" tree-shaking from that library of lots of small little ESM modules, you don't need to rely on maintainers building their own minified bundles.

The obvious trade-off, of course, is that HTTP 1.0 wasn't optimized well for lots of little files and even HTTP 1.1 servers haven't always been best configured for connection reuse and pipelining. Bundling is still sometimes useful for a number of reasons (whether or not minification matters or compile-time treeshaking makes a noticeable difference from "natural" runtime treeshaking). Of course, all the browsers that support importmaps and script type="module" all support HTTP 2 and most support HTTP 3 and that trade-off dynamic shifts again with those protocols in play (the old "rules" that you must bundle for performance stop being "rules" and everything becomes a lot more complex and needs "on the ground" performance eyeballs).

Seems like an incredibly ineffcient way to build a SPA. Fine for small pet project though but if you plan to build a SaaS or something this is just silly