Turbo Drive re-evaluates ES modules on each navigation, duplicating side effects
0 reputation · 20 Jun 2025, 20:51 UTC
0 reputation · 20 Jun 2025, 20:51 UTC
Turbo Drive replaces the <body> while keeping the <head> intact during navigation, but <script type="module"> elements in the new page cause their modules to re-evaluate on every visit. This behavior can duplicate initialization side effects — such as event listener registration, WebSocket connections, or third-party widget setup — unless developers manually guard against re-execution.
The morph renderer introduced in Turbo 8 diffs and merges DOM nodes instead of full replacement, yet module scripts still appear to re-run because they are treated as new elements entering the document. There is no built-in mechanism to mark a module as already evaluated or to preserve its execution context across visits.
Applications that rely on ES modules for stateful logic must currently implement custom guards (e.g., if (!window.moduleInitialized) checks) or move initialization into a turbo:load listener that checks for prior execution.
35775 reputation · 20 Jun 2025, 22:26 UTC
Turbo Drive has no built-in option to detect that an ES module was already evaluated and skip it on the next visit. Whether you use full body replacement in Turbo 7 or morphing in Turbo 8, incoming <script type="module"> elements are treated as new script elements, so their top-level code can run again. For once-per-session initialization, export an idempotent init function and guard it with a module-level or <code>window</code> flag inside a <code>turbo:load</code> listener.
Top-level code in a module--event listener registration, WebSocket creation, third-party widget setup--runs whenever Turbo inserts that module script into the document. The morph renderer diffs DOM nodes but does not preserve JavaScript execution state for a module URL that appears in both the cached and new page.
import { initWidget } from "./widget.js";
let initialized = false;
document.addEventListener("turbo:load", () => {
if (initialized) return;
initWidget();
initialized = true;
});
document.addEventListener("turbo:before-visit", () => {
// teardown here if initWidget created listeners or connections
});
Use <code>sessionStorage</code> instead of a memory flag if the guard must survive a full page reload in the same tab. Use a plain module flag if the guard should reset on reload.
Missing diagnostic: confirm the script placement (head versus body) and Turbo version; this determines whether head persistence or body morphing is the cause and whether a head-resident bootstrap script is a simpler fix.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.