Qué es el HMR: actualizar módulos sin recargar ni perder el estado
El Hot Module Replacement sustituye en caliente el módulo que editas sin recargar la página ni tirar el estado de la aplicación. Qué lo distingue del live reload, por qué el estado preservado transforma el bucle de desarrollo, y cómo lo orquesta Vite 8.
Editas el color de un botón dentro de un formulario a medio rellenar, con un modal abierto y el scroll a mitad de página. Guardas. El botón cambia y todo lo demás sigue exactamente donde estaba: el formulario con sus datos, el modal abierto, el scroll intacto. Eso es el HMR, y una vez lo interiorizas cuesta volver a programar sin él. No es un truco de comodidad: es un cambio en la naturaleza del bucle entre escribir código y ver su efecto.
- Definir el HMR y separarlo con precisión del full reload y del live reload.
- Entender por qué preservar el estado cambia el bucle de desarrollo, no solo su velocidad.
- Situar el HMR como un servicio del dev server, ausente en el build de producción.
- Reconocer las tres piezas que lo hacen posible: servidor, canal y runtime en el cliente.
Del full reload al reemplazo en caliente
Hay tres niveles de inmediatez entre guardar un archivo y ver el cambio, y confundirlos es la fuente de casi todos los malentendidos sobre el HMR.
- Recarga manual. Editas, cambias a la ventana del navegador y pulsas refrescar. La página se reconstruye desde cero.
- Live reload. Un observador de archivos detecta el cambio y dispara por ti un
location.reload(). Es más rápido de disparar, pero igual de destructivo: la página reinicia, todo el grafo de módulos se reejecuta desde el punto de entrada y todo el estado en memoria muere. - HMR. El dev server envía a la página en marcha solo el módulo que cambió; el runtime lo intercambia en su sitio mientras el resto de la aplicación sigue corriendo, intacta, con su estado vivo.
La diferencia esencial no es la velocidad: es que el live reload destruye el runtime y el HMR lo preserva. Una recarga completa reejecuta tu grafo entero desde el entry; el HMR reejecuta un módulo.
Conviene ver el live reload no como una versión más lenta del HMR, sino como su opuesto conceptual. El live reload trata tu aplicación como un documento que se vuelve a pedir entero; el HMR la trata como un proceso vivo al que se le cambian piezas por dentro. La primera es la metáfora del navegador de los noventa —pedir una página y renderizarla—; la segunda es la metáfora de un sistema en ejecución que se parchea sin apagarlo, más cercana a cómo se actualiza el software de una centralita o de un satélite que a cómo se recarga un sitio web.
flowchart LR edit[Editas un modulo] --> watch[El dev server detecta el cambio] watch --> reload[Live reload recarga toda la pagina] watch --> hmr[HMR envia solo el modulo cambiado] reload --> dead[Estado destruido y grafo reejecutado] hmr --> alive[Estado vivo y solo un modulo reejecutado] style dead fill:#f38ba8,color:#11111b style alive fill:#a6e3a1,color:#11111b
El estado que no quieres perder
Al trabajar, el estado de la interfaz se acumula: un asistente en el paso 4, una tabla filtrada y ordenada, un lienzo con un dibujo a medias, un chat desplazado hasta cierto mensaje, una sesión autenticada, una ruta anidada muy adentro de la app. Para reproducir ese estado tras una recarga completa tienes que reconstruirlo a mano, clic a clic, en cada iteración. Ese es el impuesto real: no los milisegundos de la recarga, sino la reconstrucción manual del contexto antes de cada intento. El HMR lo elimina: te quedas exactamente en el estado donde vive el bug y iteras sobre él directamente.
Estado de navegación
La ruta activa, el historial, los parámetros. Tras una recarga vuelves al inicio y navegas de nuevo hasta la pantalla del bug.
Estado de formularios
Campos rellenados, pasos de un asistente, validaciones a medias. Reconstruirlos a mano en cada iteración es el impuesto más caro.
Estado efímero de UI
Modales abiertos, acordeones desplegados, scroll, foco, selección. Nada de esto vive en una URL, así que una recarga lo borra sin remedio.
Estado de conexión
Sesiones, WebSockets abiertos, datos en memoria de un store. Volver a montarlos cuesta tiempo real de red en cada recarga.
Hay también un ángulo cognitivo. El bucle interno —editar, ver, ajustar— es donde un desarrollador pasa la mayoría de sus horas. Cuando ese bucle es de menos de un segundo y no destruye nada, permaneces en flujo: el código se siente como un material vivo que moldeas, no como un programa que reinicias sin cesar. Cuando cada iteración cuesta una recarga más una reconstrucción, la atención se rompe una y otra vez. El HMR convierte la página en un medio que responde.
Esta es la razón por la que el HMR se volvió una expectativa y no un lujo: una vez que el bucle interno es instantáneo y no destructivo, cualquier herramienta que te obligue a reconstruir el contexto en cada cambio se siente rota, aunque hace diez años fuera lo normal. El listón subió, y subió para siempre.
Un cambio a un módulo de lógica pura también se propaga sin recargar, siempre que algo en la cadena de importadores lo acepte. Lo que hace tan visible al HMR es el estado de la vista, pero el mecanismo es general: cualquier módulo puede intercambiarse en caliente. La interfaz es donde más se nota, no el único sitio donde ocurre.
Tres piezas: servidor, canal, runtime
El HMR es un protocolo distribuido entre el dev server y la página en marcha. Tres piezas lo sostienen.
- Servidor. Vigila el sistema de archivos, mantiene el grafo de módulos, calcula a qué módulos afecta una edición y empuja una actualización. En Vite 8 el grafo y el HMR son por entorno gracias a la Environment API: cliente, SSR en Node y edge tienen cada uno su grafo y su canal.
- Canal. Un transporte, casi siempre un WebSocket que abre el cliente. El servidor manda cargas de actualización —
js-update,css-update,full-reload,prune— y el cliente también puede responder. - Runtime. El módulo
@vite/client, inyectado en la página, recibe las cargas, vuelve a importar la nueva versión del módulo con una marca de tiempo fresca e invoca los callbacks que ese módulo registró conimport.meta.hot.
El baile entre las tres piezas ocurre en milisegundos y sigue siempre la misma coreografía: el servidor detecta el cambio, recalcula el conjunto de módulos afectados y envía por el canal una carga que nombra qué actualizar y con qué marca de tiempo. El runtime importa dinámicamente cada módulo nombrado usando esa marca como parámetro de consulta —así el navegador no sirve la versión cacheada— y ejecuta los callbacks de aceptación. Ni el servidor sabe qué hará el cliente con el módulo, ni el cliente sabe cómo lo calculó el servidor: el canal es la única frontera, y esa separación es la que deja funcionar al HMR igual con cualquier framework encima.
// forma de una carga de actualizacion que el servidor envia por el canal
{
type: "update",
updates: [
{ type: "js-update", path: "/src/Boton.tsx", timestamp: 1730000000 }
]
}
// import.meta.hot solo existe en desarrollo; en el build es undefined
if (import.meta.hot) {
import.meta.hot.accept((nuevo) => {
// aplica la nueva version sin recargar la pagina
render(nuevo.estado);
});
}
import.meta.hot es undefined en el build. Envolver el bloque en if (import.meta.hot) permite al bundler eliminarlo entero por tree shaking, de modo que ni una línea de tu código de HMR llega a producción. El HMR es, por diseño, una comodidad exclusiva del desarrollo que se desvanece sin dejar rastro en el artefacto final.
Configurar y observar el canal
El canal es un WebSocket, y como todo WebSocket puede fallar al conectarse cuando hay un proxy inverso, HTTPS, un túnel o un contenedor de por medio. Por eso Vite expone server.hmr para ajustar cómo el cliente encuentra al servidor: el puerto que anunciar al navegador, el host, el protocolo o la ruta del socket. El síntoma de una mala configuración es inconfundible: guardas y no pasa nada, o el navegador reconecta en bucle sin aplicar cambios.
// vite.config.ts: ajustar el canal cuando el HMR corre tras un proxy
import { defineConfig } from "vite";
export default defineConfig({
server: {
hmr: {
overlay: true, // errores a pantalla completa sobre la app
clientPort: 443, // el puerto que ve el navegador, no el interno
protocol: "wss", // WebSocket seguro tras un proxy TLS
},
},
});
Este ajuste importa más de lo que parece en cuanto sales de localhost. Si pruebas la app desde el móvil en tu red local, desde un contenedor Docker o a través de un túnel público, el navegador vive en un host distinto del que Vite cree, y el WebSocket apunta al sitio equivocado. Alinear host y clientPort con la dirección que realmente ve el navegador es lo que restaura el HMR en esos escenarios, y una de las causas más comunes de ese HMR que funciona en tu máquina pero no en la del compañero.
El runtime también emite eventos que puedes escuchar para instrumentar el propio HMR. import.meta.hot.on("vite:beforeUpdate", ...) se dispara antes de aplicar una actualización, vite:afterUpdate después, vite:beforeFullReload justo antes de una recarga completa, y vite:ws:disconnect cuando se cae el canal. Enganchar estos eventos es la forma de depurar por qué una actualización no llega o por qué la página recarga cuando esperabas un cambio local.
Con server.hmr: false apagas el HMR y caes a recargas completas. Rara vez lo quieres en desarrollo local, pero es útil cuando depuras un problema de estado que solo aparece en un arranque limpio, o cuando un entorno de red hostil vuelve el WebSocket más molesto que útil. Saber que el interruptor existe es parte de dominar la herramienta.
Históricamente, el bucle entre escribir código y verlo correr tenía un suelo duro: recompilar, recargar, reconstruir el contexto. Cada iteración era un pequeño reinicio del mundo. El HMR empuja ese suelo hacia cero y, sobre todo, lo vuelve no destructivo, y esa segunda propiedad es la profunda. No es una mejora incremental de velocidad: es un cambio de fase en la relación entre el programador y el programa. Cuando la conexión entre una edición y su efecto es inmediata y conserva el estado, el código deja de ser un objeto que ejecutas y pasa a ser un material que manipulas en vivo, con las manos dentro del sistema mientras funciona. Bret Victor lo formuló como principio: los creadores necesitan una conexión inmediata con lo que crean, y toda latencia o discontinuidad en esa conexión es una idea que no llegaste a tener. Pero hay que entender el otro lado del trato. El HMR compra esa inmediatez renunciando a la garantía de correctitud de un arranque limpio: parchea un sistema vivo cuyo estado actual podría ser incompatible con el código nuevo. Por eso vive solo en el dev server y desaparece en producción, donde lo que quieres es exactamente lo contrario, un build determinista construido desde cero. Comprender el HMR como lo que es —una optimización deliberadamente permeable, inmediatez a cambio del riesgo de estado rancio— es lo que te deja confiar en él sin sorprenderte cuando, de vez en cuando, se rinde y recarga la página entera. Esa rendición no es un fallo: es el sistema eligiendo correctitud en el instante exacto en que la inmediatez se volvió insegura.
- Construye un contador, ábrelo, increméntalo unas veces y edita su etiqueta. Guarda y comprueba que la cuenta sobrevive al cambio.
- Provoca una recarga completa editando algo que la fuerce, y mide el coste de reconstruir el mismo estado a mano.
- Envuelve un módulo con efectos en
if (import.meta.hot)y confirma en el build de producción que el bloque desapareció por completo. - Abre las herramientas del navegador, inspecciona los frames del WebSocket mientras editas y lee una carga
js-updatereal. - Pon
server.hmr.overlayenfalsey compara la experiencia ante un error de evaluación con y sin overlay a pantalla completa. - Escucha
vite:beforeUpdateyvite:beforeFullReloadconimport.meta.hot.ony registra en consola cada actualización y cada recarga.