Turbo (part of Hotwire) and µJS solve the same problem: make server-rendered websites feel faster without rewriting the frontend in JavaScript. Both intercept link clicks and form submissions, fetch pages via AJAX, and inject content into the DOM.
The differences are in scope, weight, and how much they ask of your server.
Size
| Library | Size (min + gzip) |
|---|---|
| µJS | ~5 KB |
| Turbo | ~25 KB |
Turbo is 5× heavier. For a library whose primary job is "fetch a page and swap some HTML", that's a significant gap.
Build step
Turbo requires a build step — it's distributed as an npm package designed to be bundled. µJS doesn't:
<!-- µJS: drop-in, no bundler needed -->
<script src="https://unpkg.com/@digicreon/mujs/dist/mu.min.js"></script>
<script>mu.init();</script>
This matters for projects that deliberately avoid a JavaScript build pipeline — static sites, PHP/Python/Ruby apps, or any project where adding npm + a bundler is a step backwards.
Server-side requirements
This is the most important difference in practice.
µJS requires nothing from your server. It sends a standard HTTP request and expects standard HTML in return. Your existing pages work as-is.
Turbo has conventions. Turbo Drive (basic navigation) works like µJS. But Turbo Frames require your server to return specific <turbo-frame> elements. Turbo Streams — the equivalent of µJS's patch mode — requires your server to return <turbo-stream> custom elements wrapping content in <template> tags.
This means adopting Turbo Streams changes your server-side HTML output. With Rails and the Hotwire ecosystem, helpers handle this for you. With other backends, you're on your own.
Multi-fragment updates: patch mode vs Turbo Streams
Both libraries can update multiple parts of the page in a single response. The syntax tells the story.
µJS — patch mode:
The server returns plain HTML. Each fragment carries mu-patch-target and mu-patch-mode attributes:
<!-- Append new comment -->
<div class="comment" mu-patch-target="#comments" mu-patch-mode="append">
<p>Great article!</p>
</div>
<!-- Update counter -->
<span mu-patch-target="#comment-count">14 comments</span>
<!-- Reset form -->
<form action="/comments" method="post" mu-patch-target="#comment-form">
<textarea name="body"></textarea>
<button>Submit</button>
</form>
Turbo Streams:
Each fragment must be wrapped in <turbo-stream> / <template>:
<turbo-stream action="append" target="comments">
<template>
<div class="comment">
<p>Great article!</p>
</div>
</template>
</turbo-stream>
<turbo-stream action="update" target="comment-count">
<template>14 comments</template>
</turbo-stream>
<turbo-stream action="replace" target="comment-form">
<template>
<form action="/comments" method="post">
<textarea name="body"></textarea>
<button>Submit</button>
</form>
</template>
</turbo-stream>
With µJS, the fragment is the content. With Turbo, each fragment requires a wrapper structure. The µJS approach has less boilerplate, and the HTML is directly readable as-is.
There's another practical advantage: with µJS, mu-patch-target attributes are ignored on initial page load. You can use the exact same HTML fragment in your normal page template and in your patch response.
HTTP methods
Turbo supports GET and POST only. µJS supports GET, POST, PUT, PATCH, and DELETE — directly on links, buttons, and forms via mu-method:
<!-- DELETE button -->
<button mu-url="/api/item/42" mu-method="delete"
mu-mode="remove" mu-target="#item-42">
Delete
</button>
<!-- PUT link -->
<a href="/api/publish/5" mu-method="put" mu-mode="none">Publish</a>
With Turbo, PUT/PATCH/DELETE require a workaround (hidden _method field or server-side routing conventions).
Triggers and polling
Turbo handles links and forms. That's it.
µJS adds mu-trigger, which lets any element initiate a fetch on any event:
<!-- Live search -->
<input mu-trigger="change" mu-debounce="300"
mu-url="/search" mu-target="#results" mu-mode="update">
<!-- Poll every 5 seconds -->
<div mu-trigger="load" mu-repeat="5000"
mu-url="/notifications" mu-target="#notifs" mu-mode="update">
<!-- Load on focus -->
<input mu-trigger="focus"
mu-url="/suggestions" mu-target="#suggestions" mu-mode="update">
Real-time: SSE
Both libraries support Server-Sent Events. µJS has it built-in and reuses the same patch syntax:
<div mu-trigger="load" mu-url="/stream"
mu-method="sse" mu-mode="patch"></div>
The server pushes standard HTML fragments with mu-patch-target attributes — the same format as a regular patch response. Nothing new to learn.
Turbo Streams can be delivered over SSE, but you're still working with the <turbo-stream> / <template> format, and the server must produce that structure.
When Turbo makes more sense
Turbo is the right choice if:
- You're in the Rails / Hotwire ecosystem — the integration is deep, the helpers are mature, and the community is large
- You need Turbo Native for iOS/Android apps
- Your team is already familiar with Turbo conventions
Outside the Rails ecosystem, Turbo's conventions become overhead without the ecosystem benefits.
Summary
| µJS | Turbo | |
|---|---|---|
| Size | ~5 KB | ~25 KB |
| Build step | None | Required |
| Server changes needed | No | For Frames and Streams |
| Multi-fragment updates | Patch mode (plain HTML) | Turbo Streams (<turbo-stream>) |
| HTTP methods | GET/POST/PUT/PATCH/DELETE | GET/POST |
| Triggers on any event | Yes | No |
| Debounce / Polling | Built-in | No |
| SSE | Built-in | Built-in |
| Rails ecosystem | No | Yes |
µJS is a focused library: drop it in, call mu.init(), and your site gains AJAX navigation with no server changes. If you need more, the attributes are there. If you don't, you don't pay for them.
- Live playground — test each feature interactively
- Full comparison: µJS vs Turbo vs htmx
- GitHub
- npm:
npm install @digicreon/mujs
Top comments (0)