DEV Community

Cover image for µJS vs Turbo: same idea, different philosophy
Amaury
Amaury

Posted on

µJS vs Turbo: same idea, different philosophy

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.


Top comments (0)