Migrating a Laravel Jetstream App from Webpack to Vite
For months I've been frustrated with how slow Laravel Mix (Webpack) was to compile, even on a beefy M1 Max or Ryzen machine. Current guides for migrating to Vite were unhelpful - every time I'd migrate, get stuck, revert back, and repeat.
A huge shoutout to Matt for helping me with this. We worked together on a call to migrate my project Analyse. If you're not already, give Matt a follow - he shares awesome Laravel tips and his progress on a great developer platform.
Speed Comparison
Locally on an M1 Max, I went from ~3 seconds with Mix down to 120 milliseconds with Vite during development. On our Ryzen production machine, this came down from 10 seconds to 5 seconds - a massive difference when pushing to production often.
Requirements
This is an opinionated guide using:
- Vue 3
- Inertia.js
- Laravel Jetstream
- TailwindCSS v3
- Inertia Global/Persistent Layouts
Migration
First, install the dependencies:
npm install vite @vitejs/plugin-vueThen update your package.json:
{
"scripts": {
"dev": "vite --config vite.client.config.js",
"watch": "vite --config vite.client.config.js",
"prod": "npm run production",
"production": "vite build --config vite.client.config.js"
},
"postcss": {
"plugins": {
"autoprefixer": {},
"tailwindcss": {
"config": "tailwind.cjs"
}
}
}
}After making these changes, rename your tailwindcss.config.js file to tailwind.cjs.
Next, create a new vite.client.config.js file in your project root:
import { defineConfig } from "vite";
import { resolve } from "path";
import vue from "@vitejs/plugin-vue";
export default defineConfig(({ command }) => ({
base: command === "serve" ? process.env.ASSET_URL || "" : `${process.env.ASSET_URL || ""}/build/`,
publicDir: false,
build: {
manifest: true,
outDir: "public/build",
rollupOptions: {
input: "resources/js/app.js",
},
},
resolve: {
alias: {
"@": resolve(__dirname, "resources/js"),
"/img": resolve(__dirname, "public/img"),
},
},
plugins: [vue()],
server: { fs: { allow: [`${process.cwd()}`] }, port: process.env?.VITE_PORT ?? 3000 },
}));Then replace the following tags in your blade file:
<head>
@production
@php
$manifest = json_decode(File::get(public_path('build/manifest.json')), true);
@endphp
<script type="module" src="{{ asset('build/' . $manifest['resources/js/app.js']['file']) }}"></script>
<link rel="stylesheet" href="{{ asset('build/' . $manifest['resources/js/app.js']['css'][0]) }}">
@else
@verbatim
<script type="module" src="http://localhost:3000/@vite/client"></script>
@endverbatim
<script type="module" src="http://localhost:3000/resources/js/app.js"></script>
@endproduction
</head>After making these changes, update your bootstrap file to use imports:
import _ from "lodash";
import axios from "axios";
window._ = _;
window.axios = axios;
window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";Then modify your app.js file:
import "../css/app.css";
import { createApp, h } from "vue";
import { createInertiaApp, Head, Link } from "@inertiajs/inertia-vue3";
import AppLayout from "@/Layouts/AppLayout.vue";
import { ZiggyVue } from "../../vendor/tightenco/ziggy/dist/vue.es";
let asyncViews = () => {
return import.meta.glob("./Pages/**/*.vue");
};
createInertiaApp({
title: (title) => (title != "Home" ? `${title} - JoinServers` : "The Best Minecraft Server List Website - JoinServers"),
resolve: async (name) => {
if (import.meta.env.DEV) {
let page = (await import(`./Pages/${name}.vue`)).default;
return page;
} else {
let pages = asyncViews();
const importPage = pages[`./Pages/${name}.vue`];
return importPage().then((module) => module.default);
}
},
setup({ el, App, props, plugin }) {
const VueApp = createApp({ render: () => h(App, props) });
VueApp.config.globalProperties.route = window.route;
VueApp.use(plugin).use(ZiggyVue).component("InertiaHead", Head).component("InertiaLink", Link).mount(el);
},
});
import "./bootstrap";What we're doing here:
- Globally defining the Inertia Head and Link (optional)
- Adding the
asyncViewsvariable - Changing all
requireto useimport - Adding
globalPropertiesfor Ziggy - Importing the stylesheet
The final and most important change is ensuring each component import has a .vue suffix:
import JetTable from "@/Jetstream/Table.vue";Without this change, your components or layout will not load.
Finally, delete remaining Webpack files and dependencies:
rm -rf webpack.config.js
rm -rf webpack.mix.js
npm remove laravel-mixServer-Side Rendering (SSR) Support
If search traffic is your main source of visitors, you'll need SSR. Inertia.js makes this fairly easy with a built-in server - we just need to configure Vite in SSR mode.
First, install the following packages:
npm install @vue/server-renderer @inertiajs/serverCreate an SSR vite file for serving assets from the server:
import { defineConfig } from "vite";
import { resolve } from "path";
import vue from "@vitejs/plugin-vue";
export default defineConfig(({ command }) => ({
base: command === "serve" ? process.env.ASSET_URL || "" : `${process.env.ASSET_URL || ""}/build/`,
publicDir: false,
build: {
ssr: true,
target: "node17",
outDir: "public/build-ssr",
rollupOptions: {
input: "resources/js/ssr.js",
},
},
resolve: {
alias: {
"@": resolve(__dirname, "resources/js"),
"/img": resolve(__dirname, "public/img"),
},
},
plugins: [vue()],
}));Create an ssr.js file in resources/js:
import { createSSRApp, h } from "vue";
import { renderToString } from "@vue/server-renderer";
import { createInertiaApp, Head, Link } from "@inertiajs/inertia-vue3";
import createServer from "@inertiajs/server";
import useRoute from "./Composable/useRoute";
let asyncViews = () => {
return import.meta.glob("./Pages/**/*.vue");
};
createServer((page) =>
createInertiaApp({
title: (title) => (title != "Home" ? `${title} - CharlieJoseph` : "An Example Default Title - CharlieJoseph"),
page,
render: renderToString,
resolve: (name) => {
let pages = asyncViews();
const importPage = pages[`./Pages/${name}.vue`];
return importPage().then((module) => module.default);
},
setup({ app, props, plugin }) {
const VueApp = createSSRApp({ render: () => h(app, props) });
VueApp.config.globalProperties.route = useRoute;
VueApp.use(plugin).component("InertiaHead", Head).component("InertiaLink", Link);
return VueApp;
},
})
);Create a useRoute.ts file in resources/js/Composable for Ziggy support:
import { computed } from "vue";
import { usePage } from "@inertiajs/inertia-vue3";
import baseRoute, { Config, RouteParamsWithQueryOverload } from "ziggy-js";
const locale = computed(() => usePage().props.value.locale);
const isServer = typeof window === "undefined";
let route;
if (isServer) {
const ziggy = computed(() => usePage().props.value.ziggy);
const ZiggyConfig = computed<Config>(() => ({
...ziggy.value,
location: new URL(ziggy.value.url),
}));
route = (name, params, absolute = false, config = ZiggyConfig.value) => baseRoute(name, params, absolute, config);
} else {
route = baseRoute;
}
export const localizedRoute = (routeName: string, params?: RouteParamsWithQueryOverload) => {
if (locale.value === "en") {
return route(routeName, params);
}
return route(`${locale.value}.${routeName}`, params);
};
export default route;A huge shoutout to Bruno Tomé for building and sharing this code snippet.
Now configure your Inertia server. Add the following to your app.blade.php file:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
@routes
@inertiaHead
</head>
<body class="font-sans antialiased">
@inertia
</body>
</html>Add SSR scripts to package.json:
{
"scripts": {
"dev": "vite --config vite.client.js",
"ssr-dev": "vite --config vite.ssr.config.js",
"watch": "vite --config vite.client.js",
"production": "vite build --config vite.client.js",
"ssr-production": "vite build --config vite.ssr.config.js",
"start-ssr": "node public/build-ssr/ssr.js"
}
}Publish your inertia.php file:
php artisan vendor:publish --provider="Inertia\ServiceProvider"Enable SSR in config/inertia.php:
[
'ssr' => [
'enabled' => true,
'url' => 'http://127.0.0.1:13714/render',
],
]For local testing, run in separate windows:
npm run devnpm run ssr-devnpm run start-ssr
For production:
npm run production
npm run production-ssrIf you don't have PM2, install it:
npm install pm2@latest -gStart the SSR server:
pm2 start public/build-ssr/ssr.jsThat's it - you now have SSR running for your Laravel + Inertia + Vue + Vite application.
You're done. You've moved from Webpack to Vite and should see a massive speed difference.