Vite 6 and WordPress: Faster Dev Server, Smoother Builds

WordPress theme development with webpack was always slow. Rebuilds took 3-5 seconds, HMR was flaky, and the config file was a novel. I switched to Vite 6 for my WordPress theme workflow, and the dev experience is night and day — sub-second HMR, clean config, and a production build that just works.
The Setup: Vite as WordPress Asset Builder
The key insight is that Vite doesn’t replace WordPress — it handles your JavaScript, CSS, and asset bundling while WordPress continues to serve PHP templates. In development, Vite’s dev server runs alongside WordPress, and your theme pulls assets from Vite’s server. In production, you use the built files.
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { resolve } from "path";
export default defineConfig({
plugins: [react()],
base: "/wp-content/themes/my-theme/dist/",
build: {
outDir: "dist",
manifest: true,
rollupOptions: {
input: {
main: resolve(__dirname, "src/main.tsx"),
admin: resolve(__dirname, "src/admin.ts"),
},
},
},
server: {
origin: "http://localhost:5173",
cors: true,
},
});
Enqueuing Assets: Dev vs Production
Your theme needs to load assets from Vite’s dev server during development and from the dist/ folder in production. I use a helper function that reads Vite’s manifest file:
function vite_enqueue(string $entry): void {
$dev_server = 'http://localhost:5173';
$is_dev = defined('VITE_DEV') && VITE_DEV;
if ($is_dev) {
// Dev: load from Vite dev server
wp_enqueue_script(
'vite-client',
$dev_server . '/@vite/client',
[],
null
);
wp_enqueue_script(
'theme-' . $entry,
$dev_server . '/src/' . $entry,
['vite-client'],
null
);
// Add type="module" to script tags
add_filter('script_loader_tag', function ($tag, $handle) {
if (str_starts_with($handle, 'vite-') || str_starts_with($handle, 'theme-')) {
return str_replace(' src', ' type="module" src', $tag);
}
return $tag;
}, 10, 2);
} else {
// Production: read from manifest
$manifest_path = get_template_directory() . '/dist/.vite/manifest.json';
if (!file_exists($manifest_path)) return;
$manifest = json_decode(file_get_contents($manifest_path), true);
$asset = $manifest['src/' . $entry] ?? null;
if (!$asset) return;
// Enqueue CSS
if (!empty($asset['css'])) {
foreach ($asset['css'] as $i => $css) {
wp_enqueue_style(
'theme-' . $entry . '-css-' . $i,
get_template_directory_uri() . '/dist/' . $css
);
}
}
// Enqueue JS
wp_enqueue_script(
'theme-' . $entry,
get_template_directory_uri() . '/dist/' . $asset['file'],
[],
null
);
}
}
// In functions.php
add_action('wp_enqueue_scripts', function () {
vite_enqueue('main.tsx');
});
HMR That Actually Works With PHP
Vite’s HMR works out of the box for React components and CSS modules — you edit a component, and it hot-reloads in the browser without a full page refresh. For CSS changes in Tailwind, the update is nearly instant.
The trick for PHP template changes is adding a simple browser-sync layer or using Vite’s server.watch to trigger a full reload when PHP files change:
// vite.config.ts addition
export default defineConfig({
// ... other config
server: {
watch: {
// Watch PHP files and trigger full reload
ignored: ["!**/*.php"],
},
},
plugins: [
react(),
{
name: "php-reload",
handleHotUpdate({ file, server }) {
if (file.endsWith(".php")) {
server.ws.send({ type: "full-reload" });
return [];
}
},
},
],
});
Vite 6 Environment API
Vite 6 introduced the Environment API, which lets you define separate module processing pipelines for different targets. For WordPress, this is useful when you have both frontend and admin assets that need different configurations:
// vite.config.ts with Environment API
export default defineConfig({
environments: {
client: {
build: {
outDir: "dist/client",
rollupOptions: {
input: resolve(__dirname, "src/main.tsx"),
},
},
},
admin: {
build: {
outDir: "dist/admin",
rollupOptions: {
input: resolve(__dirname, "src/admin.ts"),
external: ["jquery"], // WordPress provides jQuery
},
},
},
},
});
Handling WordPress-Specific Assets
Some WordPress quirks need special handling:
- jQuery dependency — if any of your code touches jQuery (legacy plugins, etc.), mark it as external in the build config and let WordPress enqueue it
- WordPress globals — things like
wp,wpApiSettings, andajaxurlneed to be declared in a.d.tsfile so TypeScript doesn’t complain - Public path — the
baseconfig must match where your dist folder lives relative to the WordPress root
// src/types/wordpress.d.ts
declare const wp: {
ajax: { post: (action: string, data: object) => Promise<unknown> };
i18n: { __: (text: string, domain?: string) => string };
};
declare const ajaxurl: string;
declare const wpApiSettings: { root: string; nonce: string };
Production Build
Running vite build generates hashed assets and a manifest file. The build is fast — under 2 seconds for a typical theme with React components and Tailwind CSS. Compare that to webpack builds that routinely took 15-20 seconds.
The dev server starts in under 300ms. HMR updates apply in under 50ms. After years of webpack, this speed difference changes how you work — you stop batching changes and start making small, immediate adjustments because the feedback loop is essentially instant.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.


