Skip to content

How to Build Single-File JavaScript Applications Without Build Tools

Purpose

This post shows how to build single-file JavaScript applications without build tools, bundlers, or dependencies.

Environment

  • Vanilla JavaScript (ES6+)
  • Modern browsers with native module support
  • No build tools or package managers required

Why Go Build-Free?

Modern JavaScript development often feels overwhelming. You have Webpack configs, package managers, transpilers, linters, formatters. But you don’t need any of this for many applications.

Single-file apps give you:

  • Instant development: Save file, refresh browser
  • Zero setup: No node_modules, no installation
  • Portability: One file works anywhere
  • Performance: No bundle overhead, instant loading
  • Learning: Understand fundamentals without abstractions

I think the key benefit is speed. When you eliminate build tools, you eliminate complexity. You can focus on writing code instead of configuring toolchains.

Basic Structure

Here’s what a single-file app looks like:

"app.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Single-File App</title>
<style>
/* CSS with variables for theming */
:root {
--primary: #3b82f6;
--bg: #0f172a;
--text: #f1f5f9;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui; background: var(--bg); color: var(--text); }
.container { max-width: 800px; margin: 0 auto; padding: 2rem; }
</style>
</head>
<body>
<div id="app"></div>
<script type="module">
// ES6+ modules work natively
const state = { count: 0 }
function render() {
const app = document.getElementById('app')
app.innerHTML = `
<div class="container">
<h1>Counter: ${state.count}</h1>
<button onclick="window.increment()">Increment</button>
</div>
`
}
window.increment = () => {
state.count++
render()
}
render()
</script>
</body>
</html>

This structure uses modern JavaScript features. CSS variables handle theming. ES modules organize code. Template literals make rendering cleaner. All without any build step.

Procedural Assets

One challenge with single-file apps is handling images and assets. The solution? Generate them programmatically.

Let me show you what I mean with Canvas API:

"asset-generator.js
// Generate isometric sprites with Canvas API
function createIsometricBuilding(width, height, color) {
const canvas = document.createElement('canvas')
canvas.width = 64
canvas.height = 64
const ctx = canvas.getContext('2d')
// Draw isometric cube
ctx.fillStyle = color
ctx.beginPath()
ctx.moveTo(32, 16)
ctx.lineTo(52, 24)
ctx.lineTo(52, 56)
ctx.lineTo(32, 48)
ctx.closePath()
ctx.fill()
// Top face (lighter)
ctx.fillStyle = lighten(color, 20)
ctx.beginPath()
ctx.moveTo(32, 16)
ctx.lineTo(52, 24)
ctx.lineTo(32, 32)
ctx.lineTo(12, 24)
ctx.closePath()
ctx.fill()
return canvas.toDataURL()
}
function lighten(color, percent) {
// Simple color lightening logic
return color
}

This approach is how MicroState (a 50KB city builder game) creates all its graphics. No image files. Everything is generated at runtime using Canvas API.

The advantage? Your app stays small. You don’t need to bundle images. The assets are lightweight vectors or data URIs generated from code.

Module Pattern Without Bundlers

You can still organize code without a bundler. Use the module pattern or namespace objects:

"module-system.js
// Create a simple module system
const App = (() => {
// Private state
const modules = new Map()
// Public API
return {
define(name, deps, factory) {
modules.set(name, { deps, factory })
},
require(name) {
if (!modules.has(name)) {
throw new Error(`Module ${name} not found`)
}
const module = modules.get(name)
return module.factory()
},
init() {
// Bootstrap app
console.log('App initialized')
}
}
})()
// Define modules
App.define('utils', [], () => ({
clamp: (num, min, max) => Math.min(Math.max(num, min), max),
random: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
}))
// Use modules
const utils = App.require('utils')

This pattern gives you encapsulation without requiring Webpack or Rollup. You get private state, public APIs, and dependency management all in vanilla JavaScript.

Web Components for UI

You don’t need React or Vue for component-based UI. Web Components work natively:

"todo-item.js
// Custom element without build tools
class TodoItem extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
connectedCallback() {
const text = this.getAttribute('text') || ''
this.shadowRoot.innerHTML = `
<style>
:host { display: block; padding: 0.5rem; border-bottom: 1px solid #eee; }
.text { cursor: pointer; }
.text.done { text-decoration: line-through; opacity: 0.6; }
</style>
<span class="text" onclick="this.toggle()">${text}</span>
`
}
toggle() {
this.querySelector('.text').classList.toggle('done')
}
}
customElements.define('todo-item', TodoItem)

Web Components give you:

  • Shadow DOM for style isolation
  • Custom elements for reusable UI
  • No framework dependencies
  • Native browser support

Browser APIs Replace Libraries

Modern browsers have powerful APIs that replace many libraries:

  • IndexedDB: Client-side database (replaces backend for many use cases)
  • Service Workers: Offline support
  • Canvas/WebGL: Graphics without libraries
  • Web Audio: Sound without assets
  • CSS Grid/Flexbox: Layout without frameworks

I think developers overlook these native APIs. They’re well-supported, performant, and require no build step.

Optimization Techniques

Keeping a single-file app small requires smart optimization:

  1. CSS variables: Reduce重复的样式代码
  2. Minification: Use online tools like Terser
  3. Gzip compression: Essential for size reduction (50KB → 10KB)
  4. Lazy loading: Load features only when needed
  5. Procedural generation: Create assets algorithmically

The key is writing concise but readable code. Don’t sacrifice clarity for size. Use minification tools for deployment, not development.

Real-World Example: MicroState

MicroState demonstrates what’s possible with this approach:

  • 50KB compressed entire game
  • Isometric 2.5D city builder
  • Procedurally generated graphics
  • Pure vanilla JavaScript
  • No dependencies at runtime or build time

The game uses Canvas API for rendering. It generates city layouts procedurally. It uses clever compression techniques to stay small while remaining playable.

You can play it in your browser. No installation. No build process. Just open the HTML file.

When Single-File Makes Sense

Single-file apps are perfect for:

  • Prototypes and MVPs
  • Games and interactive demos
  • Tools and utilities
  • Educational content
  • Portfolio pieces
  • Hackathon projects

Consider build tools when:

  • Team collaboration (TypeScript, linting)
  • Heavy third-party dependencies
  • Complex routing and state management
  • Production optimization needs
  • Code splitting requirements

Deployment

Drop the HTML file anywhere:

  • GitHub Pages
  • Netlify (drag and drop)
  • Vercel
  • Any static file host
  • Local file system (file://)

No build process. No CI/CD complexity. Upload and it works.

Summary

In this post, I showed how to build single-file JavaScript applications without build tools. The key point is using vanilla JavaScript and browser APIs to create self-contained apps. This approach eliminates complexity while maintaining the ability to build complete applications.

You get instant development feedback. Zero setup time. And a portable application that works anywhere. Start small, embrace constraints, and discover how much you can do with so little.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments