Skip to content

Splitting Laravel into Three Vite Bundles for Smaller Pages

A

Al Amin Ahamed

Senior Engineer

7 min read
𝕏 in

Most Vite + Laravel setups use a single bundle. That works for small apps but breaks down when you have separate frontend, auth, and admin areas — the auth bundle pulls in admin code, the public site loads the React app it doesn't need, and your vendor.js becomes 800kb of conflicting dependencies.

Here's how I split this portfolio into three independent Vite bundles with HMR working across all of them.

The Three Bundles

resources/ js/ frontend.tsx # Public site — Alpine.js + Particles, no React auth.tsx # Auth pages — React + Inertia (login, register) dashboard.tsx # Admin — React + Inertia (CMS) scss/ frontend.scss auth.scss dashboard.scss

Each bundle has its own dependencies, its own CSS, and its own entry point.

vite.config.js

import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; import path from 'path'; export default defineConfig({ plugins: [ laravel({ input: [ 'resources/js/frontend.tsx', 'resources/js/auth.tsx', 'resources/js/dashboard.tsx', 'resources/scss/frontend.scss', 'resources/scss/auth.scss', 'resources/scss/dashboard.scss', ], refresh: true, }), react(), tailwindcss(), ], resolve: { alias: { '@': path.resolve(__dirname, 'resources/js'), '@scss': path.resolve(__dirname, 'resources/scss'), '@assets': path.resolve(__dirname, 'resources/assets'), }, }, });

The Laravel plugin reads each input and creates a separate manifest entry. Vite handles the rest.

In Blade

Each layout pulls only what it needs:

{{-- layouts/app.blade.php (public site) --}} @vite(['resources/js/frontend.tsx', 'resources/scss/frontend.scss']) {{-- layouts/auth.blade.php --}} @vite(['resources/js/auth.tsx', 'resources/scss/auth.scss']) {{-- layouts/dashboard.blade.php --}} @vite(['resources/js/dashboard.tsx', 'resources/scss/dashboard.scss'])

Vite generates separate manifest entries with separate hashed filenames. The browser only downloads what the current page uses.

Conditional Inertia

Auth and dashboard share Inertia setup but the public site doesn't have it. Each bundle does its own bootstrapping:

// auth.tsx import { createInertiaApp } from '@inertiajs/react'; createInertiaApp({ resolve: (name) => import(`./Pages/Auth/${name}.tsx`), setup: ({ el, App, props }) => createRoot(el).render(<App {...props} />), }); // dashboard.tsx createInertiaApp({ resolve: (name) => import(`./Pages/Dashboard/${name}.tsx`), setup: ({ el, App, props }) => createRoot(el).render(<App {...props} />), });

The page resolvers are scoped — auth.tsx can't accidentally import a dashboard page.

Bundle Size

Before splitting (single bundle): ~620kb gzipped, every page.

After splitting:

  • Frontend: ~180kb (Alpine + utilities, no React)
  • Auth: ~240kb (React + Inertia + auth-only pages)
  • Dashboard: ~410kb (React + Inertia + CodeMirror + admin)

The public site's gain is the biggest — frontend visitors no longer download admin code.

HMR Across Bundles

This was the part that took longest to get right. With multiple bundles, Vite's HMR needs to know which bundle changed and only push updates to clients running that bundle.

The Laravel plugin handles this automatically — but only if your .env's VITE_APP_URL matches the dev server. Mismatch and HMR silently fails on auth/dashboard.

APP_URL=http://localhost:8000 VITE_APP_URL=http://localhost:5173

If HMR works on frontend.tsx but not dashboard.tsx, this is almost always the cause.

Production Build

yarn build

Output:

public/build/ manifest.json assets/ frontend-a8c.js (180kb) auth-3f1.js (240kb) dashboard-7e2.js (410kb) frontend-2bc.css auth-5d8.css dashboard-9af.css

The Laravel plugin's @vite() directive reads the manifest and outputs the right <script> tags per layout.

What I'd Avoid

  • Lazy-loading all pages. Inertia's import(...) already does this. Going further (route-level code splitting) on top adds complexity for marginal gain on this size of app.
  • Sharing a single vendor chunk across bundles. Tempting, but Vite's manualChunks API across multiple inputs gets fragile. Let each bundle have its own vendor code — duplication is fine at this scale.
  • Skipping the alias config. Without @/ paths, every move of a component cascades into 30 import path edits. Not worth it.
Share 𝕏 in
A

Al Amin Ahamed

Senior software engineer & AI practitioner. Laravel, PHP, WordPress plugins, WooCommerce extensions.

About me →

One email a month. No noise.

What I shipped, what I read, occasional deep dive. Unsubscribe anytime.