Before 2012, all of my websites were made using HTML, CSS and a sprinkling of JS. Then, I went all-in on AngularJS, followed by React. I started using Typescript and then PureScript and learned more frameworks like Halogen and Concur. I even wrote my own UI framework called purescript-deku
.
Recently, I had the chance to put together a PoC for a gaming company I founded. The PoC's goal is to learn more about social gaming patterns. Time and speed were of the essence: I had to build the game in two weeks and it had to run at 60 fps on most devices.
My first reaction was to reach for PureScript, deku
, and three.js. This was going fine for a couple days, but I quickly realized that I was hitting a gnarly wall:
- PureScript's generated code, even though it was heavily optimized, still struggled to run at 60 fps on older devices.
- A lot of my PureScript code looked & felt imperative, which is clunkier than writing straight-up imperative JS.
- Chrome's flame charts were basically unreadable, as the functional patterns I was using led to 100-function-deep charts that provided no actionable suggestions or information.
In parallel, PureFunctor had developed a small Vanilla JS engine on top of three.js with no additional UI frameworks. He showcased it in a single-page app, and the whole thing held together in less than 400 lines of code. I took his initial work and modified it slightly, adding tailwind-css
for styling and developed it into Mystery Mansion Madness, which in just a few days has been played by 100s of people.
In this article, I'd like to talk about how I un-frameworked and un-compiles-to-JS'd myself for this project, why it felt oh-so-good, and what my learnings are for future projects.
Is JS that bad after all?
A lot of discussion around JavaScript in the development community (at least those I'm part of) involves images of fire, brimstone, and pain. Phrases like "callback hell" and "painstakingly hand-crafted code" are common currency.
A friend of mine that used to work for a large soul-eating consultancy before landing a job at Spotify bemoaned having to work with legacy code-bases held together by jQuery. She's now very happy to be working on modern apps in React and TypeScript.
Given this environment, there aren't many external incentives to try writing anything in JS. But I'm not gonna lie, putting together an application in mostly-vanilla JS was pretty frickin' awesome:
-
eslint
andhtmlhint
caught 95+% of errors that would have crashed my app. - I could leave my dev server running without drowning in fan noise.
- Compile times are insanely fast.
- People could dip into the project in very little time and be productive almost immediately.
- The time from
git push
to live-in-production was less than 1m. - Making preview builds & links from my local machine took 75% less time.
- I could optimize things when needed without fear of hidden costs from framework-related cruft.
Perhaps the most refreshing change was that the browser took the place of the compiler in the dialectical back-and-forth I like to have with a machine counterpart as I write code.
I'm used to the PureScript compiler guiding me through the development process, and in general, getting the types to check will lead to a crash-free application. Here, Chrome & the Chrome console wound up filling that role. This meant that my eyes were constantly on the application and I was continuously testing it, so I was more likely to catch UI/UX warts than by interacting with a compiler. For really sensitive stuff or fragile flows, the occasional puppeteer
test wound up being all I needed.
Furthermore (and maybe this is cuz I'm no spring chicken anymore) I only have so much cognitive capacity to spar with a machine. Vanquishing the PureScript compiler is certainly a dopamine boost, but it usually leaves me in a state of mental fatigue when I then approach the actual application I'm building. As a result, I get into the bad habit of writing copious amounts of code without looking at the UI, which invariably distances me from those who consume what I'm building. Working in Vanilla JS helped me keep my eye on the (spooky 👻👻👻) prize.
Learnings from PureScript
It might sound here like I'm hating on frameworks & type-safe languages like TypeScript and PureScript, but I'm not. PureScript is by far my favorite language, and if it were as fast as JS, as easy to debug with flame charts, and built into Google Chrome, I would be using it instead for this project. But what I did take from PureScript into JS-land was a collection of patterns. I can't stress enough how important this is.
PureScript & Haskell (but mostly PureScript) taught me how to think, and those patterns are super useful in any application. In no particular order, thanks to PureScript, I found myself:
- Knowing when to curry functions for partial application.
- Knowing when & where to use Applicatives for concurrent programming via Promises.
- Being comfortable passing around functions as values when it made sense.
- Writing code that can be aggressively refactored. Originally, I thought this would not go smoothly because I'm used to letting the PureScript compiler walk me through refactoring. But functional patterns lead to composable chunks, which makes refactoring easier, and it turned out that I had enough of the types in my head (& in the comments!) to know what to change & where, and eslint does a good enough job to fill in any gaps.
- Thinking in terms of FRP and SDOM architectures, both of which I learned from the ground up in PureScript. In the case of SDOM, PureScript was and still is the bleeding edge of thought - just this week someone released a new SDOM-flavored library called
purescript-jelly
, and exciting new ideas are emerging from the PS community all the time.
I'm not saying that these things can only be learned from a compiles-to-JS language like PureScript. But different languages express different ideas with different degrees of fluidity, and by working in a language like PureScript, I found myself soaking up these ideas faster, which made them easier to apply outside of PureScript.
So I would recommend to anyone doing webdev to learn PureScript not only because making apps with it is awesome, but also because it will make you a better developer outside of PureScript.
Wish-list
In spite of these laudatory remarks regarding (mostly) vanilla JS, there were still a few little things I would have liked. Perhaps they exist already, but I couldn't find them. They're all on the order of utilities:
Elements to vars
(Jewish) Santa, please give me a watcher that looks for JS ids that follow a given pattern (ie that start with x-
) and generates a file where they're all gotten by ID, ie:
export const foo = document.getElementById('x-foo');
export const foo = document.getElementById('x-bar');
Fragment prefixing
If I have a bit of HTML:
<div id="x-my-div">
<span id="x-my-span">hi</span>
</div>
I'd like to be able to get the fragment by id, clone it, and replace ids with a given key, for example:
const mySpan = expand(myDiv).using("foo-");
Would result in:
<div id="x-foo-my-div">
<span id="x-foo-my-span">hi</span>
</div>
For dynamic content, this is really all you need, and even that is overkill for a lot of cases. For example, in Mystery Mansion Madness, new players pop up with the following snippet of code:
const data = doc.data();
let html = "";
for (var i = 1; i < 9; i++) {
if (data["player" + i + "Name"] && i !== player) {
html += `<div><span class="bg-zinc-600/80 p-2 text-white">${
data["player" + i + "Name"]
} has joined!</span></div>`;
}
}
document.getElementById(id).innerHTML = html;
A rock-solid, highly configurable router
My SPA doesn't use routing, but if it did, I would have coded my own router. There are some nice options out there for bits and pieces of a router like path-to-regexp
, but I haven't found the killer router util yet. But I admittedly haven't looked hard either...
Conclusion
If you, like me, use React, Angular, Halogen, Typescript, ReScript, PureScript, F#, Clojure, Idris or something else that is either a framework or compiles to JS, I highly recommend that for your next project, you don't use any of these and just write the JavaScript by hand. You may fall (back) in love with it and never use a framework/compiles-to-JS language again.
OTOH, if you haven't ever used a framework or compiles-to-JS language, I recommend trying them out! Especially PureScript.
What will I use for my next project? Idk, maybe I'll write a giant byte array, use it like a Turing machine, and be done with it. Or maybe Brainfuck. But odds are that it'll be vanilla JS if I do something like this again, as I'll just copy and paste this as a template. If anything, I'd like to dig deeper into Tailwind's utility-first philosophy and make a library of small composable webgl utilities so that, for example, I can get a Phong effect without using a full-blown framework like three or babylon.
Until then, enjoy Mystery Mansion Madness! and lemme know if you find any bugs. PRs are welcome, provided they're in vanilla JS 😁
Top comments (2)
Hey Mike, are you still enjoying your experience with vanilla JS?
Wild stuff! Would love to hear more about the new gaming company 😁