https://mxb.dev/blog/buildless/
skip to main content
Select Theme
* Classic
* Dark
* Koopa Beach
* Choco Mountain
* Moo Moo Farm
* Bowser's Castle
* Yoshi Valley
* Rainbow Road
* Lobster Life
* Hacker News
close themepicker
[9k] Max Bock
toggle menu
* 01 Home
* 02 Writing
* 03 Notes
* 04 About
toggle theme panel
Going Buildless
08 Sep 2024 * code
View Demo
The year is 2005. You're blasting a pirated mp3 of "Feel Good Inc"
and chugging vanilla coke while updating your website.
It's just a simple change, so you log on via FTP, edit your style.css
file, hit save - and reload the page to see your changes live.
Did that story resonate with you? Well then congrats A) you're a nerd
and B) you're old enough to remember a time before bundlers,
pipelines and build processes.
Now listen, I really don't want to go back to doing live updates in
production. That can get painful real fast. But I think it's amazing
when the files you see in your code editor are exactly the same files
that are delivered to the browser. No compilation, no node process,
no build step. Just edit, save, boom.
There's something really satisfying about a buildless workflow. Brad
Frost recently wrote about it in "raw-dogging websites", while
developing the (very groovy) site for Frostapalooza.
So, how far are we away from actually working without builds in HTML,
CSS and Javascript? The idea of "buildless" development isn't new -
but there have been some recent improvements that might get us
closer. Let's jump in.
The obvious tradeoff for a buildless workflow is performance. We use
bundlers mostly to concatenate files for fewer network requests, and
to avoid long dependency chains that cause "loading waterfalls". I
think it's still worth considering, but take everything here with a
grain of performance salt.
HTML
Permalink to "HTML" #
The main reason for a build process in HTML is composition. We don't
want to repeat the markup for things like headers, footers, etc for
every single page - so we need to keep these in separate files and
stitch them together later.
Oddly enough, HTML is the one where native imports are still an
unsolved problem. If you want to include a chunk of HTML in another
template, your options are limited:
* PHP or some other preprocessor language
* server-side includes
* frames?
There is no real standardized way to do this in just HTML, but Scott
Jehl came up with this idea of using iframes and the onload event to
essentially achieve html imports:
Andy Bell then repackaged that technique as a neat web component.
Finally Justin Fagnani took it even further with html-include-element
, a web component that uses native fetch and can also render content
into the shadow DOM.
For my own buildless experiment, I built a simplified version that
replaces itself with the fetched content. It can be used like this:
That comes pretty close to actual native HTML imports, even though it
now has a Javascript dependency .
Server-Side Enhancement
Permalink to "Server-Side Enhancement" #
Right, so using web components works, but if you want to nest
elements (fetch a piece of content that itself contains a
html-include), you can run into waterfall situations again, and you
might see things like layout shifts when it loads. Maybe progressive
enhancement can help?
I'm hosting my experiment on Cloudflare Pages, and they offer the
ability to write a "worker" script (very similar to a service worker)
to interact with the platform.
It's possible to use a HTML Rewriter in such a worker to intercept
requests to the CDN and rewrite the response. So I can check if the
request is for a piece of HTML and if so, look for the html-include
element in there:
// worker.js
export default {
async fetch(request, env) {
const response = await env.ASSETS.fetch(request)
const contentType = response.headers.get('Content-Type')
if (!contentType || !contentType.startsWith('text/html')) {
return response
}
const origin = new URL(request.url).origin
const rewriter = new HTMLRewriter().on(
'html-include',
new IncludeElementHandler(origin)
)
return rewriter.transform(response)
}
}
You can then define a custom handler for each html-include element it
encounters. I made one that pretty much does the same thing as the
web component, but server-side: it fetches the content defined in the
src attribute and replaces the element with it.
// worker.js
class IncludeElementHandler {
constructor(origin) {
this.origin = origin
}
async element(element) {
const src = element.getAttribute('src')
if (src) {
try {
const content = await this.fetchContents(src)
if (content) {
element.before(content, { html: true })
element.remove()
}
} catch (err) {
console.error('could not replace element', err)
}
}
}
async fetchContents(src) {
const url = new URL(src, this.origin).toString()
const response = await fetch(url, {
method: 'GET',
headers: {
'user-agent': 'cloudflare'
}
})
const content = await response.text()
return content
}
}
This is a common concept known as Edge Side Includes (ESI), used to
inject pieces of dynamic content into an otherwise static or cached
response. By using it here, I can get the best of both worlds: a
buildless setup in development with no layout shift in production.
Cloudflare Workers run at the edge, not the client. But if your site
isn't hosted there - It should also be possible to use this approach
in a regular service worker. When installed, the service worker could
rewrite responses to stitch HTML imports into the content.
Maybe you could even cache pieces of HTML locally once they've been
fetched? I don't know enough about service worker architecture to do
this, but maybe someone else wants to give it a shot?
CSS
Permalink to "CSS" #
Historically, we've used CSS preprocessors or build pipelines to do a
few things the language couldn't do:
1. variables
2. selector nesting
3. vendor prefixing
4. bundling (combining partial files)
Well good news: we now have native support for variables and nesting,
and prefixing is not really necessary anymore in evergreen browsers.
That leaves us with bundling again.
CSS has had @import support for a long time - it's trivial to include
stylesheets in other stylesheets. It's just ... really frowned upon.
Why? Damn performance waterfalls again. Nested levels of @import
statements in a render-blocking stylesheet give web developers the
creeps, and for good reason.
But what if we had a flat structure? If you had just one level of
imports, wouldn't HTTP/2 multiplexing take care of that, loading all
these files in parallel?
Chris Ferdinandi ran some benchmark tests on precisely that and the
numbers don't look so bad.
So maybe we could link up a main stylesheet that contains the
top-level imports of smaller files, split by concern? We could even
use that approach to automatically assign cascade layers to them,
like so:
/* main.css */
@layer default, layout, components, utils, theme;
@import 'reset.css' layer(default);
@import 'base.css' layer(default);
@import 'layout.css' layer(layout);
@import 'components.css' layer(components);
@import 'utils.css' layer(utils);
@import 'theme.css' layer(theme);
Design Tokens
Permalink to "Design Tokens" #
Love your atomic styles? Instead of Tailwind, you can use something
like Open Props to include a set of ready-made design tokens without
a build step. They'll be available in all other files as CSS
variables.
You can pick-and-choose what you need (just get color tokens or
easing curves) or use all of them at once. Open props is available on
a CDN, so you can just do this in your main stylesheet:
/* main.css */
@import 'https://unpkg.com/open-props';
Javascript
Permalink to "Javascript" #
Javascript is the one where a build step usually does the most work.
Stuff like:
* transpiling (converting modern ES6 to cross-browser supported
ES5)
* typechecking (if you're using TypeScript)
* compiling JSX (or other non-standard syntactic sugars)
* minification
* bundling (again)
A buildless worflow can never replace all of that. But it may not
have to! Transpiling for example is not necessary anymore in modern
browsers. As for bundling: ES Modules come with a built-in
composition system, so any browser that understands module syntax...
...allows you to import other modules, and even lazy-load them
dynamically:
// main.js
import './some/module.js'
if (document.querySelector('#app')) {
import('./app.js')
}
The newest addition to the module system are Import Maps, which
essentially allow you to define a JSON object that maps dependency
names to a source location. That location can be an internal path or
an external CDN like unpkg.
Any Javascript on that page can then access these dependencies as if
they were bundled with it, using the standard syntax: import { render
} from 'preact'.
Conclusion
Permalink to "Conclusion" #
So, can we all ditch our build tools soon?
Probably not. I'd say for production-grade development, we're not
quite there yet. Performance tradeoffs are a big part of it, but
there are lots of other small problems that you'd likely run into
pretty soon once you hit a certain level of complexity.
For smaller sites or side projects though, I can imagine going the
buildless route - just to see how far I can take it.
Funnily enough, many build tools advertise their superior "Developer
Experience" (DX). For my money, there's no better DX than shipping
code straight to the browser and not having to worry about some
cryptic node_modules error in between.
I'd love to see a future where we get that simplicity back.
Links
Permalink to "Links" #
* Repo for my demo code
* Getting started with buildess
* A simple dev server for a buildless workflow
* "A Real "Buildless" Modern Web Development Workflow? Oh Yes!" by
Jared White
* "Writing Javascript without a build system" by Julia Evans
Going Buildless
Max Bock [avatar2]
Published in [code] * 08 Sep 2024
Edit this Post
Follow @mxbck
Webmentions
What's this?
No webmentions yet.
Other things I've written
1. Live CMS Previews with Sanity and Eleventy 28 Aug 2024
2. Upgrading to Eleventy v3 01 Jun 2024
3. Old Dogs, new CSS Tricks 26 May 2024
4. A year in review: 2023 31 Dec 2023
5. 7 Reasons why I don't write 30 Jan 2023
6. The IndieWeb for Everyone 12 Nov 2022
7. Make Free Stuff 25 Jan 2022
8. Media Queries in Times of @container 11 Jun 2021
9. Container Queries in Web Components 15 May 2021
10. Asset Pipelines in Eleventy 26 Apr 2021
(c) 2012-2024
Max Bock
Work
Codista
Social
Mastodon / Github
Legal
Privacy Policy
Links
Blogroll
Edit this page on Github RSS Feed