Skip to content
LaravelBlog

livewire/livewire: Fix assets after a stylesheet never being loaded

LaravelBlogBot
LaravelBlogBot

Contributed by stebrl

Part of the livewire v4.4.7 release

Coming to Livewire v4 from v3.8.10, this is a bug worth knowing about before you ship: when Livewire injects @assets blocks during an update request, any asset that directly follows a non-script asset — almost always a stylesheet — was silently dropped. PR #10482 fixes this on the 4.x branch (a rebase of the same change submitted in #10458) with a two-line adjustment to how the asset injector iterates the parsed <head>. Nothing about the @assets API changes, but if you use it, the fix can quietly re-enable scripts that have never been running on the deferred parts of your app.

What changed

The fix lives in js/features/supportScriptsAndAssets.js, the feature responsible for merging <head> assets that arrive with a Livewire update into the current page. The loop that applies those assets iterated a live DOM collection:

for (let child of newHead.children) {
    await runAssetSynchronously(child)
}

It now snapshots the collection first:

// Take a static copy of the children. Appending a non-script asset moves it
// out of this document, and iterating the live collection while it shrinks
// would skip the asset right after it...
let children = [...newHead.children]

for (let child of children) {
    await runAssetSynchronously(child)
}

That is the entire runtime change, plus a new browser test, test_assets_after_a_stylesheet_are_not_skipped, that locks the behaviour in.

The symptom: the stylesheet loads, the script never runs

This code path only runs for assets injected by an update request — in practice, components rendered conditionally, into a modal, or behind a toggle:

@assets
    <link rel="stylesheet" href="/plugin.css">
    <script src="/plugin.js"></script>
@endassets

Before the fix, the <link> was applied but the <script> was skipped. Nothing throws and the component renders fine; only whatever the script provided is missing — window.Plugin is not defined, an editor that never initialises, a picker that stays a plain input.

Bundler output makes this especially painful, because a @vite() directive expands to preload links followed by the module script: the links load, the script that does the actual work is skipped. The existing tests happened to declare the script before the stylesheet — the one ordering that worked — which is why this survived as long as it did.

Root cause: a live collection that shrinks under the iterator

newHead is the head of a parsed document adopted into the live one. runAssetSynchronously() clones script tags (leaving the original in place), but appends every other element directly to the document head — and appending moves the node out of newHead. Because newHead.children is a live HTMLCollection, it shrinks by one as each non-script element is moved, and every remaining element shifts down an index. The loop's next index then lands one element too far.

With [link, script], the link is moved, the script shifts to index 0, the loop asks for index 1 and finds nothing. The try/catch around the loop body exists for asset load errors, so the skip is completely silent. Snapshotting the children with a spread breaks the coupling between iteration and DOM mutation.

What this means for your upgrade

There are no API, config, or template changes to make — the fix is internal to the injector. The impact is behavioural:

  • Scripts that were being dropped will now load and execute. If you worked around the bug — reordering the block so the script came first, or manually re-including the script elsewhere on the page — those workarounds are now redundant. Watch for double-initialisation if you also bootstrapped the plugin by hand.
  • This is not a regression introduced by the upgrade itself. It was originally found in a production app on 4.3.3 and is latent in the injector logic, so it is something you carry across the upgrade rather than pick up from it.
  • Make sure the v4 release you adopt includes this patch. Any build without it will keep skipping the asset that follows a stylesheet during update injection.

How it's verified

The new browser test renders a deferred nested component whose @assets block is a stylesheet followed by a script, loads it via a wire:click toggle, and asserts that window.assetAfterStylesheetLoaded === true — a flag only the second script sets. Against the unpatched base commit the test times out waiting for the script; with the fix it passes. The wider suites are green too: 1,329 unit tests, 101 JavaScript tests, and the SupportScriptsAndAssets, SupportNavigate, SupportLazyLoading, SupportJsModules, and SupportAutoInjectedAssets browser suites all pass unchanged.

Takeaways:

  • If a @assets block lists a stylesheet before a script, pre-fix v4 silently skipped the script when the block arrived with an update; the fix loads it.
  • The cause was iterating a live HTMLCollection while elements were being moved out of it; the fix is a static [...newHead.children] snapshot.
  • Initial page renders were never affected — only update-injected assets from deferred, modal, or toggled components.
  • After upgrading, remove ordering workarounds and check for double-initialisation of scripts that now actually run.
  • Confirm the release you upgrade to includes PR #10482.

Sources

More from this release

Related Articles