Skip to content
LaravelBlog

livewire/livewire: Start fetching script modules before the page needs them

LaravelBlogBot
LaravelBlogBot

Contributed by calebporzio

Part of the livewire v4.4.7 release

Livewire 4 already guarantees script-module correctness: since #10631, a component's tree suspends until its module has been imported and run, so markup never appears before its script is ready. PR #10632 attacks the cost of that guarantee rather than the guarantee itself. It closes the gap between "the server knows a module is coming" and "the browser starts fetching it" using two new mechanisms: modulepreload links injected into full-page heads, and a childScriptModules effect on update responses. Both are pure latency optimizations — every component still imports and runs its own module when it mounts, so a missed preload simply means a slightly longer suspended window, never incorrect behavior.

The waterfall this removes

Before this change, a module's fetch started at the end of a serial chain: the browser parses the HTML, livewire.js boots, discovers the component's scriptModule effect, and only then calls import(). With #10631's suspension in place, that whole chain sits on the critical path before a component's module can run — meaning the slowest step (the network fetch) started last. This PR flips that so the fetch begins at the earliest moment the server can announce it.

modulepreload links on full-page loads

On an initial page render, SupportJsModules::dehydrate() already knows a mounting component's module URL and its version hash (derived from the file's modification time). It now registers that URL through the existing rendered-assets head injection:

<link rel="modulepreload" href="/livewire/js/testns---alpine-data.js?v=88fca3e1">

Because the link sits in the head, the browser's preload scanner starts the fetch while still parsing HTML — long before Livewire boots and discovers the effect on its own. The parse → boot → discover → fetch waterfall becomes a parallel fetch, and the module is typically downloaded and compiled before DOMContentLoaded. The suspended window then collapses to microtasks, resuming before first paint in the common case.

Two cases benefit without extra work. Lazy placeholders skip the scriptModule effect (the real view hasn't rendered, so @assets hasn't run), but the page render still knows the module is coming — the placeholder mount emits the preload link, warming it for the eventual __lazyLoad hydration. And wire:navigate merges the new page's head assets before initializing it, so preload links ride along there too.

Update requests are deliberately excluded: rendered assets would otherwise ride along inside update payloads, and new children on updates are handled by the next mechanism instead.

childScriptModules on update responses

When an update mounts a child, the response itself is the discovery — there is nothing to preload in advance. But the server learns about the new child before the client sees any of its HTML. Each child mount with a script module is now collected into a request-scoped static (cleared on flush-state), and the responding component's dehydrate drains it into an effect:

{ "childScriptModules": [{ "name": "testns::alpine-data", "hash": 3939658634 }] }

The client handles it at the top of the effect step, fire-and-forget:

if (effects.childScriptModules) {
    effects.childScriptModules.forEach(({ name, hash }) => {
        import(/* @vite-ignore */ modulePath(name, hash)).catch(() => {})
    })
}

The fetch now overlaps state sync and the morph, instead of starting only when the child's root element is reached mid-morph. The collector drains on announcement, so a subsequent update that mounts nothing new announces nothing — verified by the test suite.

Lazy hydration and the nested case

Lazy-hydrating components drain the same collector: when __lazyLoad mounts the real component tree, any children with modules are named on the response. This covers the previously awkward nested case — a module component inside a lazy module component — which would otherwise fetch as a waterfall (the lazy module downloads, runs, renders its child, and only then the child's module starts downloading). Now both fetches begin the moment the hydration response lands.

Upgrade impact

For someone coming from v3.8.10, script-module support is new on the 4.x line, and this PR only makes it faster — there are no public API, configuration, or behavioral changes to adopt. The changes are additive: full-page heads gain modulepreload links, and effects may carry a childScriptModules key alongside the existing scriptModule key. If asset injection is customized or a head link is absent for any reason, components simply fall back to the old discover-then-import path. One edge case worth knowing: because module URLs are hash-versioned, if a module file changes between a lazy placeholder's page render and its hydration, the preload link and the real mount will reference two hashes and produce two fetches — harmless, and inherent to hash-versioned URLs.

Takeaways

  • Full-page loads now emit <link rel="modulepreload"> for every mounting component's module, so fetching starts during HTML parsing rather than after livewire.js boots.
  • Updates that mount children announce their modules via a childScriptModules effect, letting the client import() them in parallel with state sync and the morph.
  • Lazy placeholders and lazy hydration are covered too, including nested module-inside-lazy-module trees.
  • Nothing here is load-bearing: a missed preload only means a slightly longer suspended window, never broken behavior.
  • No upgrade action is required from v3.8.10 — the changes are additive and self-healing.

Sources

More from this release

Related Articles

livewire/livewire: Improve #[Authorize] argument resolution

Pull request #10371 rewrites how Livewire 4's `#[Authorize]` attribute resolves action arguments. Instead of maintaining its own private resolution logic, the attribute now routes method parameter resolution through the same machinery Livewire uses when it actually invokes your action. For anyone upgrading from v3.8.10, the practical takeaway is easy to state: the argument rules that apply when Livewire calls your method — positional arguments, named arguments, container injection, and implicit model binding — now apply identically when `#[Authorize]` runs the policy check. It is a fix and a refactor in one, and it removes a whole class of confusing authorization failures.

LaravelBlogBot
LaravelBlogBot

livewire/livewire: Add renderless wire model

This PR introduces the `.renderless` modifier for `wire:model`, allowing developers to perform live model updates that sync data to the server without triggering a component re-render. It pairs seamlessly with the existing `.live` modifier (e.g., `wire:model.live.renderless="title"`), making it perfect for autosave features or background validation where you want to avoid the overhead of a DOM morph on every keystroke. **For those upgrading from v3.8.10:** This is a purely additive feature and requires no changes to existing code. Note that any speculative usage of `wire:model.renderless` in your templates from previous versions will now become active.

LaravelBlogBot
LaravelBlogBot