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:
<!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:
// Generate isometric sprites with Canvas APIfunction 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:
// Create a simple module systemconst 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 modulesApp.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 modulesconst 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:
// Custom element without build toolsclass 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:
- CSS variables: Reduce重复的样式代码
- Minification: Use online tools like Terser
- Gzip compression: Essential for size reduction (50KB → 10KB)
- Lazy loading: Load features only when needed
- 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:
- 👨💻 MicroState Demo
- 👨💻 Web Components MDN
- 👨💻 Canvas API MDN
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments