Single-File Web Apps with Bun's Browser Compilation
Bun has always been about speed and developer experience, but version 1.3.10 introduces a feature that fundamentally changes how we think about frontend deployment: the ability to compile your entire application into a single, self-contained HTML file using --compile --target=browser.
This feature bridges the gap between server-side simplicity and client-side richness, allowing developers to ship web applications as easily as they ship scripts. No more complex deployment pipelines, CDN configurations, or worrying about relative paths. Just one file, ready to run in any browser.
The Problem with Modern Frontend Deployment
Modern frontend development is powerful, but the deployment story has become increasingly complex. A typical React or Vue application requires a build step, a bundler configuration, and a hosting solution that can serve multiple static assets—JavaScript bundles, CSS files, images, and fonts. Each of these assets introduces potential points of failure:
- Relative path issues: Assets might load in development but fail in production due to base URL misconfigurations.
- CORS headaches: Fonts and other resources often require specific CORS headers.
- Cache management: Updating one asset might break compatibility with cached versions of others.
- Deployment complexity: You need a server or CDN capable of serving the correct MIME types and routing.
While tools like Vite and Next.js have made this manageable, there's an undeniable elegance to the idea of a single file that contains everything your application needs.
Introducing --compile --target=browser
Bun 1.3.10 extends its powerful compilation capabilities to the browser. Previously, bun build --compile was used to create standalone executables for server-side applications—single binary files that included the Bun runtime and your code. Now, with the --target=browser flag, you can create standalone HTML files that include your entire frontend application.
The command is simple:
bun build ./src/index.tsx --compile --target=browser --outfile=app.html
This single command takes your TypeScript and JSX source files, bundles them using Bun's native bundler, and inlines everything—JavaScript, CSS, and assets—into a single HTML file.
What Gets Inlined?
Bun intelligently handles different types of content:
- JavaScript/TypeScript: Transpiled to browser-compatible JavaScript and inlined in
<script>tags. - CSS: Inlined in
<style>tags, avoiding external HTTP requests. - Assets: Small assets like images can be base64-encoded and inlined as data URLs.
This results in a file that is completely self-contained. You can email it, drop it in a public folder, or serve it from the simplest static file server.
Creating Your First Single-File App
Let's walk through building a simple counter application that compiles to a single HTML file.
Project Setup
First, create a new project directory and initialize it:
mkdir bun-single-file-counter
cd bun-single-file-counter
bun init
The Application Code
Create an index.tsx file with a simple counter component:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
fontFamily: "system-ui, sans-serif",
marginTop: "50px"
}}>
<h1>Bun Single-File Counter</h1>
<p style={{ fontSize: "48px", margin: "20px 0" }}>{count}</p>
<div>
<button onClick={() => setCount(c => c - 1)}>-</button>
<button onClick={() => setCount(c => c + 1)} style={{ marginLeft: "10px" }}>+</button>
</div>
</div>
);
}
// Render to the DOM
const root = document.getElementById("root");
if (root) {
// Bun's JSX transform automatically handles React imports
const { createRoot } = await import("react-dom/client");
createRoot(root).render(<Counter />);
}
Notice that we're using React and JSX. Bun handles the JSX transformation automatically, and the bundler includes the necessary React libraries.
Compilation
Now, compile it:
bun build ./index.tsx --compile --target=browser --outfile=counter.html
Open counter.html in your browser. That's it. No node_modules, no package.json dependencies to install on the server, no build artifacts to upload separately. Just one file.
Performance Characteristics
One might wonder about the performance implications of inlining everything. Bun addresses several concerns:
Startup Time
Because everything is in a single file, there are no network round-trips to fetch additional modules. The browser parses the HTML and immediately executes the embedded scripts. For small to medium applications, this can result in faster time-to-interactive compared to traditional multi-file bundles.
Caching Trade-offs
The trade-off is cache granularity. In a traditional setup, if you update your JavaScript, the user only needs to re-download the changed bundle. With a single-file approach, the entire file must be re-downloaded.
However, this trade-off is acceptable for:
- Internal tools: Dashboards and admin panels where cache optimization is less critical.
- Prototypes and demos: Quick iterations without infrastructure concerns.
- Offline-capable apps: Single files are easier to distribute and archive.
- Email attachments: Yes, you can email a working web app.
Bundle Size
Bun's bundler is aggressive about tree-shaking. Unused code is eliminated, and the resulting inline scripts are minified. While the single file might be larger than a split bundle, the difference is often negligible for smaller applications.
Use Cases and Benefits
1. Prototyping and Demos
Quickly share working prototypes without setting up hosting. Send the HTML file via Slack, email, or drop it in a shared drive. Recipients simply open it in their browser—no server required.
2. Internal Tools
Many internal tools are simple CRUD interfaces. Deploying them as single files on an internal network share or static file server reduces infrastructure overhead and security surface area.
3. Educational Content
Educators can distribute complete, working examples as single files. Students don't need to run npm install or configure build tools—they just open the file.
4. Offline Applications
Combine this with Service Workers or PWA capabilities, and you have an application that can be archived, distributed on USB drives, or used in air-gapped environments.
5. Serverless Edge Functions
Deploy single-file HTML applications to edge functions without worrying about asset bundling. The function simply returns the pre-compiled file.
Comparison with Traditional Bundlers
| Feature | Bun --target=browser | Vite/Webpack |
|---|---|---|
| Setup | Zero config | Requires config files |
| Output | Single HTML file | Multiple JS/CSS/HTML files |
| Runtime | None (static file) | Dev server + production build |
| Asset handling | Inline everything | External files with hashing |
| Deployment | Upload one file | Upload entire dist/ folder |
| Hot reload | Manual (re-build) | Built-in dev server |
Bun's approach isn't meant to replace traditional bundlers for large-scale applications where code splitting and cache optimization are critical. Instead, it offers a simplified alternative for the vast number of smaller projects and internal tools that don't need that complexity.
Technical Implementation
Under the hood, Bun's bundler performs several steps:
- Dependency Resolution: Bun analyzes your
importandrequirestatements, resolving dependencies fromnode_modules. - Transpilation: TypeScript and JSX are transpiled to plain JavaScript using Bun's native transpiler (written in Zig).
- Tree Shaking: Unused exports are removed to minimize bundle size.
- Minification: Code is minified to reduce file size.
- Inlining: The final JavaScript and CSS are embedded into the HTML template using
<script>and<style>tags.
This entire process happens in milliseconds, thanks to Bun's native implementation. Unlike bundlers written in JavaScript, Bun's bundler doesn't pay the overhead of the JavaScript runtime during the build process.
Limitations and Considerations
While powerful, this feature has some constraints:
- Large Assets: Inlining large images or videos can bloat the file size. Consider hosting these externally.
- Dynamic Imports: The compilation targets a static snapshot. Runtime dynamic imports from external URLs still work, but won't be inlined.
- Web Workers: Workers require separate files and aren't suited for single-file compilation.
- Server-Side Rendering (SSR): This is a client-side feature. SSR still requires a server runtime.
Future Implications
The ability to compile to a single HTML file opens interesting possibilities for the ecosystem:
- Bun-native Static Site Generators: Tools that pre-render pages at build time and compile each to a single HTML file.
- Email-based Apps: Distributing lightweight tools via email as attachments.
- Decentralized Applications: Storing and sharing applications on distributed file systems (IPFS, etc.) becomes trivial when an app is a single file.
Conclusion
Bun's --compile --target=browser is a return to simplicity in an ecosystem that has become increasingly complex. It doesn't replace the sophisticated toolchains needed for large-scale applications, but it provides a much-needed alternative for the thousands of smaller projects, prototypes, and internal tools that make up a significant portion of our daily work.
By eliminating the friction between writing code and deploying it, Bun continues to fulfill its promise of making JavaScript development faster—not just in execution speed, but in the speed of the entire development lifecycle.
Try it yourself. Take that side project you've been meaning to share, compile it with Bun, and send it to a colleague. Just one file. That's the power of Bun.