Skip to content
How I split this portfolio into three independent Vite bundles (frontend, auth, dashboard) — separate dependencies, sepa...

Splitting Laravel into Three Vite Bundles for Smaller Pages

Al Amin Ahamed

Al Amin Ahamed

Senior Software Engineer

· Updated 15 hours ago 7 min read

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

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:

BLADE
{{-- 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:

TSX
// 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.

ENV
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

BASH
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
Al Amin Ahamed

Al Amin Ahamed

Senior software engineer & AI practitioner. 5+ years shipping Laravel platforms, WordPress plugins, WooCommerce extensions, and AI-driven products.

About me →

More from the blog

Need this kind of work shipped?

Available for freelance and consulting.

Laravel platforms, WordPress plugins, WooCommerce extensions, and AI integrations.