The problem: Alpine's plugin list (Focus, Collapse, Persist, Intersect, Anchor, Mask, Morph) looks appealing, but installing all of them "just in case" bloats the JS bundle for a Laravel app that might only ever need one or two — and some overlap with things Livewire already handles.
Environment: Laravel 11, Alpine.js 3.x (Livewire-bundled), Vite 5, npm.
The Error
// Not a runtime error — a bundle-size/redundancy problem.
// Symptom: checking `npm run build` output and seeing
// app.js grow noticeably after adding plugins you're
// not actually using directives from.
vite v5.x building for production...
✓ 142 modules transformed.
public/build/assets/app-a1b2c3d4.js 187.42 kB │ gzip: 61.20 kB
// (up from ~140 kB before adding three unused plugins)
Why It Happens
Each official Alpine plugin is its own small package that adds new directives/magics on top of core Alpine. They're not tree-shaken automatically just by being imported — if you import and register a plugin via Alpine.plugin(...), its code ships in your bundle whether or not you end up using its directives on a given page. In a Laravel + Livewire app specifically, a few of these plugins also solve problems Livewire itself already solves server-side, which makes them redundant rather than complementary.
The Fix
Install only the plugins that solve something core Alpine + Livewire genuinely can't, and register them via alpine:init so they attach to Livewire's Alpine instance:
@alpinejs/collapse — worth it. Smooth expand/collapse transitions (accordions, "show more" sections) that would otherwise need manual CSS max-height hacks:
// npm install @alpinejs/collapse
// resources/js/app.js
import collapse from '@alpinejs/collapse';
document.addEventListener('alpine:init', () => {
Alpine.plugin(collapse);
});
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open" x-collapse>
Long content that expands/collapses smoothly...
</div>
</div>
@alpinejs/focus — worth it for modals/dropdowns. Handles focus trapping (keeping Tab navigation inside an open modal) correctly, which is fiddly to get right manually and matters for accessibility:
// npm install @alpinejs/focus
import focus from '@alpinejs/focus';
Alpine.plugin(focus);
<div x-show="modalOpen" x-trap="modalOpen">
<input type="text">
<button @click="modalOpen = false">Close</button>
</div>
@alpinejs/persist — situational. Persists Alpine state to localStorage across page loads (e.g. remembering a collapsed sidebar). Skip this if the same preference should also survive across devices/sessions server-side — in that case store it as a user preference in your database via Livewire/a controller instead, since persist only lives in that one browser.
<div x-data="{ sidebarOpen: $persist(true) }">
<!-- fine for pure UI preference, not for anything
that should sync across the user's devices -->
</div>
@alpinejs/intersect — situational. Useful for lazy-loading or reveal-on-scroll animations, but if you're paginating a long list, prefer Livewire's wire:scroll-style infinite scroll patterns or standard pagination over rebuilding it with Intersect + client-side fetch, since Livewire already has server-aware primitives for that.
@alpinejs/morph / @alpinejs/anchor — usually skip in a Livewire project. Morph overlaps heavily with what Livewire already does for DOM diffing on updates, and Anchor (positioning floating elements) is only worth adding if you're not already using a Tailwind UI kit or headless library that handles positioning for you.
Step-by-Step
- List every Alpine directive/magic your project actually uses in templates (
x-collapse,x-trap,$persist, etc.) by searching your Blade files. - Install only the plugin packages matching directives you found in step 1 —
npm install @alpinejs/collapse @alpinejs/focusetc. - Register each installed plugin inside
document.addEventListener('alpine:init', () => { Alpine.plugin(x) })inapp.js. - Rebuild with
npm run buildand compare the output bundle size against your previous build. - For anything Livewire already solves server-side (persisted preferences, scroll-based pagination, DOM morphing), prefer the Livewire-native approach over adding an Alpine plugin for the same job.
- Remove any plugin import that isn't backed by an actual directive used somewhere in your Blade views.
@alpinejs/persist stores data in localStorage, which is per-browser, not per-user-account — if a user switches browsers or clears site data, persisted state silently resets to defaults with no error. Never use $persist for anything the user would consider "their settings" in a meaningful sense; reserve it for cosmetic, low-stakes UI state only.Not sure which plugin (if any) actually fits your use case? Ask on the Contact page and describe what you're building.