
Usar ?v con un timestamp que cambia en cada petición impide que el navegador y el CDN cacheen tus CSS/JS. Aprende por qué ocurre y cómo solucionarlo con versionado estable o por mtime.
TL;DR
- Poner
?v=<?=time();?>en los assets cambia el URL en cada request. - Resultado: 0% de reutilización de caché, más descargas, peor LCP/FCP.
- Soluciones: versión fija por despliegue,
filemtime(), hash en nombre de archivo, y cabeceras de caché largas.
¿Qué es el parámetro v y por qué se usa?
Añadir ?v=<?time();?> a un archivo estático (CSS/JS) sirve para forzar la actualización cuando ese archivo cambia. El navegador ve un URL distinto y vuelve a descargarlo.
Bien usado, mejora el control de invalidación. Mal usado, se convierte en un freno de rendimiento.
/res/build/app.css?v=<?time();?>
/res/build/bundle.js?v=<?time();?>
El problema de ?v=<?=time();?>
Cuando el valor de v se genera con el tiempo actual en cada request:
- El URL de
/res/app.css?v=169...nunca se repite. - El navegador/CDN no pueden cachearlo de forma efectiva.
- Cada visita descarga de nuevo todos los CSS/JS: más latencia, más ancho de banda y peor experiencia.
Síntomas que verás en DevTools (Network):
- Columnas “Size” marcadas como from disk cache / memory cache desaparecen.
- Cada refresh: 200 OK en vez de 304 Not Modified o caché local.
- Tiempos de render (FCP/LCP) y TBT peor de lo esperado.
Cómo solucionarlo (sin perder control de invalidación)
1) Versión fija por despliegue
- Define una versión estática (ej. fecha del release):
v=2025-09-26. - Solo cambia cuando haces un nuevo despliegue.
- Ventaja: invalida todos los assets a la vez; simple de mantener.
Ejemplo de URL:
/res/build/app.css?v=2025-09-26
/res/build/bundle.js?v=2025-09-26
2) Versión automática por fecha de modificación (mtime)
- Usa la marca de tiempo del archivo como versión: si el archivo cambia, cambia el valor.
- Ventaja: invalida solo el asset que se modificó.
Idea en pseudocódigo:
v = mtime('/ruta/a/app.css') // p.ej. 1737935400
<link rel="stylesheet" href="/res/build/app.css?v=1737935400">
3) Hash en el nombre del archivo (fingerprinting)
- Empaquetadores modernos generan nombres como
app.33c3168.css. - Al cambiar el contenido, cambia el nombre del archivo.
- Ventaja: URLs inmutables y caché agresiva a 1 año sin riesgos.
Ejemplo:
/res/build/app.33c3168.css
/res/build/vendor.2d03cf9.js
Configura bien las cabeceras de caché
Una vez que tus URLs son estables, pon caché fuerte:
- Para CSS/JS versionados:
Cache-Control: public, max-age=31536000, immutable(1 año; elimmutableevita revalidaciones innecesarias) - Para HTML (documento principal):
Cache-Control: no-cache, must-revalidate(el HTML se revalida, pero sus assets estáticos se sirven desde caché)
Impacto en rendimiento (qué esperar)
- Menos transferencias: el navegador reutiliza CSS/JS entre páginas y visitas.
- Menor TTFB aparente de assets y mejor FCP/LCP.
- Ahorro en CDN y ancho de banda del servidor.
Buenas prácticas y checklist
- ❌ Evita timestamps que cambien en cada request.
- ✅ Usa versión fija por release o
mtime/hash. - ✅ Unifica la estrategia para todos tus assets (CSS, JS, fuentes, imágenes críticas).
- ✅ Activa
Cache-Controllargo para archivos versionados. - ✅ Verifica en DevTools que la segunda carga muestre cache hits.
¡Va! Aquí tienes ejemplos cortos para cada solución.
1) Versión fija por despliegue
<?php
define('ASSET_VER', '2025-09-26'); // cambia en cada release
?>
<link rel="stylesheet" href="/res/build/app.css?v=<?php echo ASSET_VER; ?>">
<script src="/res/build/app.js?v=<?php echo ASSET_VER; ?>"></script>
2) Versión automática por mtime (fecha de modificación)
<?php
function asset_url(string $rel): string {
$abs = __DIR__ . '/' . ltrim($rel, '/');
$ver = is_file($abs) ? filemtime($abs) : time(); // fallback simple
return '/' . ltrim($rel, '/') . '?v=' . $ver;
}
?>
<link rel="stylesheet" href="<?php echo asset_url('res/build/app.css'); ?>">
<script src="<?php echo asset_url('res/build/app.js'); ?>"></script>
3) Hash en el nombre del archivo (fingerprinting con manifest)
manifest.json (generado por tu build):
{
"app.css": "app.33c3168.css",
"app.js": "app.2d03cf9.js"
}
PHP para resolver desde el manifest:
<?php
function asset_from_manifest(string $logical): string {
static $m = null;
if ($m === null) {
$m = json_decode(file_get_contents(__DIR__ . '/res/build/manifest.json'), true) ?? [];
}
return '/res/build/' . ($m[$logical] ?? $logical);
}
?>
<link rel="stylesheet" href="<?php echo asset_from_manifest('app.css'); ?>">
<script src="<?php echo asset_from_manifest('app.js'); ?>"></script>
(Recomendado) Cabeceras de caché para assets versionados
Apache (.htaccess)
<FilesMatch "\.(css|js|png|jpg|svg|woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
Nginx
location ~* \.(css|js|png|jpg|svg|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
Con esto:
- Fija: invalida todo al cambiar la versión del deploy.
- mtime: invalida solo el asset que cambió.
- hash: invalida por contenido y permite caché “fuerte” por 1 año.
Preguntas frecuentes
¿Puedo usar timestamps en desarrollo?
Sí, para forzar recargas durante el desarrollo es cómodo. No lo lleves a producción.
¿Y si un proxy intermedio ignora query strings?
Con hash en el nombre del archivo eliminas esa preocupación y maximizas compatibilidad.
¿Qué pasa si olvido cambiar la versión fija?
El navegador seguirá sirviendo la versión anterior. Por eso, automatiza el cambio o usa mtime/hash.
Conclusión
El parámetro v es útil, pero solo si su valor permanece estable entre peticiones. Cambiarlo cada vez con un timestamp destruye la caché y el rendimiento. Adopta versión por release, mtime o hash en el nombre, y acompáñalo con cabeceras de caché adecuadas. Tu sitio —y tus usuarios— lo notarán.
