How to Set Up Vite HMR in WordPress (And Why I Switched from Laravel Mix)
If you’ve been working with WordPress block themes lately, you probably know the feeling.
You tweak some SCSS.
You save.
You wait.
The page reloads.
Your scroll resets.
Your block preview flickers.
Repeat 100 times a day.
That was me.
I had been using Laravel Mix for years in my WordPress themes. It worked fine — especially for classic themes. But once I started working more deeply with block themes, Full Site Editing, and heavier SCSS structures… my workflow started to feel slow.
Not broken. Just… friction-heavy.
So I decided to rethink my setup.
That’s when I moved to Vite with real HMR.
And it completely changed how development feels.
The Real Problem with My Old Setup
My previous stack looked like this:
- Laravel Mix
- Webpack under the hood
- Sometimes Browsersync for reloads
- Full page refresh on every change
It wasn’t terrible.
But with block themes:
- The editor reloads constantly
- Scroll position resets
- Inspector panels collapse
- Block previews flash
- State disappears
When you’re styling blocks or testing layout changes, those reloads kill your flow.
What I wanted was simple:
👉 Change CSS
👉 See update instantly
👉 No reload
👉 No state loss
That’s exactly what Vite gives you.
Why I Moved from Laravel Mix to Vite
For years, Laravel Mix was my default setup for WordPress themes.
It worked. It was stable. It was familiar.
But over time, a few things started to feel outdated:
- Slower rebuild times
- Full page reloads
- Heavier dependency tree
- More configuration overhead
- Webpack complexity I didn’t really need anymore
When I switched to Vite, the difference was immediate:
- Lightning-fast dev server
- Native ES modules
- True HMR (not just reloads)
- Simpler config
- Smaller dependency footprint
And honestly, once you experience 50ms CSS updates without reload… there’s no going back.
Why Use Vite HMR in WordPress?
Traditional WordPress workflows (Laravel Mix + Browsersync) usually mean:
- 2–3 second refresh cycles
- Scroll position reset
- Form state lost
- Flashing page reloads
With Vite HMR:
- CSS updates instantly
- No full page reload
- Scroll stays where it is
- Forms keep their state
- Console logs stay intact
In my case, CSS updates went from ~2000ms to ~50ms.
That’s roughly 40x faster feedback.
The Architecture (Simple and Clean)
The setup is intentionally minimal:
- Run Vite dev server (port 5173)
- Create a small
public/hotfile when it’s running - WordPress detects that file
- Load assets from Vite in development
- Load compiled assets in production
No environment toggles.
No manual switching.
No proxy setup.
Step 1 – Configure Vite for WordPress
In vite.config.js:
server: {
host: 'localhost',
port: 5173,
strictPort: true,
cors: true,
hmr: {
host: 'localhost',
protocol: 'ws',
},
watch: {
usePolling: true,
},
origin: 'http://localhost:5173',
}
Why this matters
strictPort: true→ Prevents unexpected port changes.cors: true→ WordPress loads scripts cross-origin.usePolling: true→ Critical on Windows/WSL.- WebSocket (
ws) → Enables real HMR.
Without proper server config, WordPress + Vite can break easily.
Step 2 – The Hot File Strategy
To detect whether Vite is running, I use a small custom plugin that creates:
public/hot
When Vite starts → file is created
When Vite stops → file is removed
WordPress simply checks:
file_exists('/public/hot');
That’s it.
No cURL.
No environment variables.
No proxy detection.
Clean and extremely reliable.
Step 3 – Load Dev Assets with HMR
When the hot file exists, WordPress loads:
<script type="module" crossorigin src="http://localhost:5173/@vite/client"></script>
<script type="module" crossorigin src="http://localhost:5173/resources/scripts/frontend/main.ts"></script>
Important details:
@vite/clientmust load first- Scripts must use
type="module" - Register scripts before declaring dependencies
- Add
crossorigin
If everything works, your console will show:
[vite] connected.
Now when you edit SCSS…
⚡ Instant update. No reload.
Step 4 – Configuring the Vite Helper in WordPress
To make everything clean and reusable, I created a small Vite helper function inside the theme.
Instead of hardcoding dev vs production logic everywhere, I centralize it.
The idea is simple:
- If
public/hotexists → load assets from Vite dev server - Otherwise → load compiled files from
/public
Here’s a look of the class I’ve created:
final class ViteHelper
{
private const VITE_DEV_SERVER = 'http://localhost:5173';
private const VITE_CLIENT = '@vite/client';
/**
* Verifica si Vite dev server está corriendo
*
* @return bool
*/
public static function is_dev_server_running(): bool
{
static $is_running = null;
if ($is_running !== null) {
return $is_running;
}
// Método 1: Verificar si el archivo hot existe (Vite lo crea cuando está corriendo)
$hot_file = get_template_directory() . '/public/hot';
if (file_exists($hot_file)) {
$is_running = true;
return $is_running;
}
// Método 2: Intentar conexión directa al puerto
$fp = @fsockopen('localhost', 5173, $errno, $errstr, 1);
if ($fp) {
fclose($fp);
$is_running = true;
return $is_running;
}
$is_running = false;
return $is_running;
}
/**
* Encola assets de Vite (dev server con HMR o archivos compilados)
*
* @param string $handle Handle único para el asset
* @param string $entry_path Ruta al entry point (ej: 'resources/scripts/frontend/main.ts')
* @param array $deps Dependencias
* @param bool $in_footer Cargar en footer
* @return void
*/
public static function enqueue_asset(
string $handle,
string $entry_path,
array $deps = [],
bool $in_footer = true
): void {
if (self::is_dev_server_running()) {
// Modo desarrollo: cargar desde Vite dev server con HMR
self::enqueue_dev_assets($handle, $entry_path, $deps, $in_footer);
} else {
// Modo producción: cargar assets compilados
self::enqueue_prod_assets($handle, $deps, $in_footer);
}
}
/**
* Encola assets desde Vite dev server (desarrollo con HMR)
*
* @param string $handle
* @param string $entry_path
* @param array $deps
* @param bool $in_footer
* @return void
*/
private static function enqueue_dev_assets(
string $handle,
string $entry_path,
array $deps,
bool $in_footer
): void {
// Registrar y encolar Vite client para HMR
wp_register_script(
'vite-client',
self::VITE_DEV_SERVER . '/' . self::VITE_CLIENT,
[],
null,
false
);
wp_enqueue_script('vite-client');
// Registrar y encolar entry point desde Vite dev server
wp_register_script(
$handle,
self::VITE_DEV_SERVER . '/' . $entry_path,
['vite-client'],
null,
$in_footer
);
wp_enqueue_script($handle);
// Marcar ambos scripts como módulos ES6
add_filter('script_loader_tag', function ($tag, $script_handle, $src) use ($handle) {
if ($script_handle === 'vite-client' || $script_handle === $handle) {
// Agregar type="module" y crossorigin
$tag = str_replace('<script', '<script type="module" crossorigin', $tag);
}
return $tag;
}, 10, 3);
}
/**
* Encola assets compilados (producción)
*
* @param string $handle
* @param array $deps
* @param bool $in_footer
* @return void
*/
private static function enqueue_prod_assets(
string $handle,
array $deps,
bool $in_footer
): void {
$theme_dir = get_stylesheet_directory();
$theme_uri = get_stylesheet_directory_uri();
// CSS compilado
$vite_css = $theme_dir . '/public/css/style.css';
if (file_exists($vite_css)) {
wp_enqueue_style(
$handle . '-css',
$theme_uri . '/public/css/style.css',
$deps,
(string) filemtime($vite_css)
);
}
// JS compilado
$vite_js = $theme_dir . '/public/js/main.js';
if (file_exists($vite_js)) {
wp_enqueue_script(
$handle . '-js',
$theme_uri . '/public/js/main.js',
$deps,
(string) filemtime($vite_js),
$in_footer
);
}
}
}
That’s it.
No conditionals scattered across the theme.
No duplicated logic.
No messy environment flags.
Just one helper that decides everything.
Production Mode
When running:
npm run build
Vite compiles assets into /public.
Then WordPress loads:
/public/css/style.css/public/js/main.js
With automatic cache busting:
filemtime($file_path)
Search engines and users see a completely normal WordPress site.
HMR only exists in development.
What’s Next: A WordPress Theme Boilerplate with Vite
After refining this setup across projects, my next step is building a modern WordPress theme boilerplate powered by Vite.
The idea:
- Clean architecture
- Built-in HMR
- Optimized production build
- Sensible defaults
- Minimal dependencies
- Ready for Docker or local environments
- Developer-first structure
Something lightweight, opinionated, and modern — without unnecessary abstraction.
I’ve used Laravel Mix for years, but moving to Vite feels like the right long-term direction for WordPress theme development.
And the upcoming boilerplate will reflect that.
Final Thoughts
Switching from Laravel Mix to Vite wasn’t just a tooling change.
It improved:
- Speed
- Simplicity
- Developer experience
- Project maintainability
If you’re building custom WordPress themes in 2026, integrating Vite with HMR is one of the best upgrades you can make.
And if you’re still on Laravel Mix…
It might be time. 🚀