Turbo Performance: Caching, Prefetching, and Morphing
We've covered Turbo Drive for navigation, Turbo Frames for partial updates, and Turbo Streams for real-time changes. But how does Turbo actually achieve SPA-level speed while rendering on the server?
This post covers the performance techniques that make Turbo fast: caching, prefetching, morphing, and more.
Why Turbo Feels Fast
Turbo's speed comes from avoiding work, not doing work faster. Traditional page loads tear everything down and rebuild from scratch. SPAs avoid that by never leaving the page, but they front-load massive JavaScript bundles. Turbo takes a middle path: server-rendered HTML, but smart enough to skip redundant work.
The key optimizations:
1. Snapshot cache — Instant back/forward navigation
2. Prefetching — Download pages before users click
3. Morphing — Update only what changed, preserve state
4. Head merging — Don't re-download CSS/JS
5. Lazy loading — Defer expensive content
Snapshot Cache
Turbo's most powerful feature. Every page you visit gets saved in memory. When you hit the back button, Turbo shows the cached snapshot instantly—no network request.
How it works:
1. You visit Page A
2. You click a link to Page B
3. Turbo saves a snapshot of Page A (cloned DOM)
4. Turbo fetches and renders Page B
5. You click back
6. Turbo shows cached Page A instantly
7. Turbo fetches fresh Page A in background
8. If anything changed, Turbo updates the page
Result: back/forward navigation feels native-app fast. The cache holds the last 20 pages by default.
Cache control:
Some pages shouldn't be cached. Add a meta tag:
<!-- Don't cache this page at all --> <meta name="turbo-cache-control" content="no-cache"> <!-- Cache, but don't show as preview --> <meta name="turbo-cache-control" content="no-preview">
Use
no-cache for pages with flash messages, sensitive data, or time-sensitive content. Use no-preview for pages where stale previews would confuse users.Prefetching
Since Turbo v8, links are prefetched when the user hovers over them. By the time they click, the page is already downloaded.
Timeline without prefetch:
Hover → Click → Fetch (500ms) → Render → Done
Timeline with prefetch:
Hover → Fetch starts (background) Click → Already cached → Render → Done (instant)
Turbo waits 100ms after hover before prefetching, avoiding wasted requests from brief hovers.
Disabling prefetch:
For expensive pages or slow connections, disable prefetching:
<!-- Disable globally --> <meta name="turbo-prefetch" content="false"> <!-- Disable for specific links --> <a href="/heavy-report" data-turbo-prefetch="false">Generate Report</a> <!-- Disable for a section --> <div data-turbo-prefetch="false"> <a href="/expensive-page">...</a> </div>
You can also intercept prefetches programmatically:
document.addEventListener("turbo:before-prefetch", (event) => {
if (navigator.connection?.saveData) {
event.preventDefault() // Don't prefetch on data-saver mode
}
})
Morphing
By default, Turbo replaces the entire
<body> when navigating. Fast, but it loses state: form inputs, focus, scroll position, video playback.Morphing is smarter. Instead of replacing, Turbo diffs the old and new DOM and updates only what changed. Turbo uses the Idiomorph library for this.
What morphing preserves:
• Input values the user typed
• Focus on the active element
• Scroll position
• Video/audio playback position
• JavaScript references to DOM elements
Enabling morphing for page refreshes:
<head> <meta name="turbo-refresh-method" content="morph"> </head>
This enables morphing when Turbo Streams trigger a page refresh, or when you call
Turbo.visit() with action: "replace".Morphing in Turbo Streams:
<turbo-stream action="replace" method="morph" target="user-form">
<template>
<form id="user-form">
<input name="email" value="alice@example.com">
<div class="error">Invalid email format</div>
</form>
</template>
</turbo-stream>
User's input is preserved even as validation errors appear.
Permanent elements:
For elements that should never change during navigation:
<div id="audio-player" data-turbo-permanent> <audio src="/radio.mp3" autoplay></audio> </div>
Permanent elements are completely skipped during morphing. Must have a unique
id.Head Merging
When navigating, Turbo merges the
<head> elements instead of replacing them. If both pages include the same stylesheet:<link rel="stylesheet" href="/styles.css">
Turbo keeps the existing one—no re-download, no flash of unstyled content.
Only new or changed elements get added. This is why you should fingerprint your assets:
<script src="/application-abc123.js" defer></script>
When you deploy new code, the filename changes, and Turbo knows to load the new version.
Lazy Loading Frames
Frames with
loading="lazy" don't fetch until they scroll into view:<turbo-frame id="comments" src="/posts/1/comments" loading="lazy"> <p>Loading comments...</p> </turbo-frame>
The initial page loads faster because expensive below-the-fold content isn't fetched until needed. Turbo uses IntersectionObserver with a 100px margin—content starts loading just before it becomes visible.
Frames Reduce Transfer
Full page fetches transfer everything: header, footer, sidebar, navigation. Frames fetch only what's inside the frame:
<!-- Without frames: fetch entire page --> <a href="/products?page=2">Next</a> <!-- With frames: fetch just the product list --> <turbo-frame id="products"> <a href="/products?page=2">Next</a> </turbo-frame>
If your full page is 50KB but the product list is 5KB, that's 90% less data transferred for pagination.
Progressive Enhancement
Turbo enhances existing pages rather than replacing them. This has performance benefits:
No build step: SPAs need bundlers, transpilers, minifiers. Turbo is one script tag.
HTML streams as it arrives: The browser renders HTML incrementally. The header appears while the footer is still downloading. SPAs wait for the entire JavaScript bundle, then fetch data, then render.
No hydration: SPAs that server-render need to "hydrate"—re-run JavaScript to attach event handlers. With Turbo, HTML is just HTML. No hydration delay.
Smaller bundle: Turbo is roughly 25KB gzipped. Compare to React + Router + state management, which easily hits 100-200KB+ before your application code.
When Turbo Might Not Be Fastest
Turbo isn't always the right choice:
Highly interactive apps: Figma, Google Docs, games. Lots of client-side state, real-time collaboration, complex UI interactions. Use React/Vue.
Offline-first apps: Turbo requires server connectivity. For apps that work offline, use service workers + SPA approach.
Very large pages: If your HTML is 500KB+, Turbo's body swap might be slower than incremental React updates.
Turbo is for navigational apps—content sites, dashboards, admin panels, e-commerce—not stateful apps like collaborative editors.
Performance Tips
Use frames for partial updates:
<turbo-frame id="search-results"> <!-- Only this updates when filtering --> </turbo-frame>
Enable morphing for forms:
<turbo-stream action="update" method="morph" target="form"> <!-- Validation errors appear without losing input --> </turbo-stream>
Lazy-load below the fold:
<turbo-frame id="analytics" src="/analytics" loading="lazy"> Loading charts... </turbo-frame>
Use streams for multi-part updates:
<!-- One form submission, three DOM updates --> <turbo-stream action="append" target="comments">...</turbo-stream> <turbo-stream action="update" target="count">...</turbo-stream> <turbo-stream action="remove" target="form"></turbo-stream>
Disable cache for sensitive pages:
<meta name="turbo-cache-control" content="no-cache">
The Bottom Line
Turbo achieves SPA-level speed through:
• Snapshot cache — Back button is instant
• Prefetching — Pages load before you click
• Morphing — Updates preserve state
• Head merging — Assets aren't re-downloaded
• Lazy frames — Expensive content waits until visible
• Smaller bundle — Less JavaScript to parse
For most web applications—content sites, dashboards, e-commerce, admin panels—Turbo delivers SPA-like speed with a fraction of the complexity. No build step, no client-side state management, no JSON APIs.
Server renders HTML. Turbo makes it fast.
Series Wrap-Up
This series covered:
1. Why Turbo — The problem with SPAs and the HTML-over-the-wire alternative
2. Turbo Drive — Drop-in navigation acceleration
3. Turbo Frames — Scoped navigation and partial updates
4. Turbo Streams — Real-time updates without complexity
5. Performance — How it all stays fast
The core insight: you don't need JSON APIs and client-side rendering to build fast, modern web applications. HTML over the wire is simpler, often faster, and easier to maintain.
Ready to try it? Start with the Turbo Handbook.
References
- Building Your Turbo Application
- Navigate with Turbo Drive (caching, prefetching)
- Page Refreshes (morphing)
- Turbo Attributes Reference
- Idiomorph (morphing library)