Ver contenido
Guía Completa de Optimización de TBT: Eliminando el Bloqueo del Hilo Principal
Introducción
Total Blocking Time (TBT) mide el tiempo acumulado de bloqueo causado por Long Tasks entre First Contentful Paint (FCP) y Time to Interactive (TTI). Un TBT alto significa que tu página se siente poco responsiva durante la fase crítica de carga.
Esta guía proporciona estrategias avanzadas de optimización para reducir TBT, basadas en la documentación oficial de Chrome Developers y las mejores prácticas de web.dev.
¿Nuevo en TBT? Comienza con nuestra guía para principiantes: 📖 Lee: TBT Explicado
Entendiendo los Long Tasks en Profundidad
¿Qué Hace que una Tarea sea “Larga”?
El hilo principal del navegador maneja todo: parsear HTML, ejecutar JavaScript, manejar entrada del usuario y pintar píxeles. Cuando una sola tarea toma más de 50 milisegundos, se convierte en un “Long Task” que bloquea el hilo principal.
Línea de Tiempo del Hilo Principal:
┌────────────────────────────────────────────────────────────────┐
│ [Parse HTML] [Ejecutar JS - 150ms] [Paint] [Clic Ignorado] │
│ └─── Long Task ───┘ └─ Input bloqueado ─┘│
└────────────────────────────────────────────────────────────────┘
Cálculo de TBT en Detalle
Para cada Long Task, la porción de bloqueo es el tiempo que excede los 50ms:
// Ejemplo: Calculando TBT de duraciones de tareas
const tareas = [
{ duracion: 250 }, // 200ms bloqueo (250 - 50)
{ duracion: 40 }, // 0ms bloqueo (< 50ms umbral)
{ duracion: 90 }, // 40ms bloqueo (90 - 50)
{ duracion: 155 }, // 105ms bloqueo (155 - 50)
];
const tbt = tareas.reduce((total, tarea) => {
const bloqueo = Math.max(0, tarea.duracion - 50);
return total + bloqueo;
}, 0);
// TBT = 200 + 0 + 40 + 105 = 345ms
Impacto de TBT en la Puntuación de Lighthouse
Según la documentación de Chrome Developers, TBT contribuye 30% a tu puntuación general de rendimiento en Lighthouse:
| Métrica | Peso | Umbral Bueno |
|---|---|---|
| FCP | 10% | < 1.8s |
| Speed Index | 10% | < 3.4s |
| LCP | 25% | < 2.5s |
| TBT | 30% | < 200ms |
| CLS | 25% | < 0.1 |
Una mejora de TBT de 600ms a 200ms puede aumentar significativamente tu puntuación general.
Estrategia 1: Dividir Long Tasks
La forma más efectiva de reducir TBT es dividir tareas grandes en chunks más pequeños que cedan el control al hilo principal.
Usando setTimeout para Ceder Control
// ❌ Mal: Una tarea larga sincrónica
function procesarItems(items) {
items.forEach(item => {
computacionPesada(item); // Bloquea hilo principal
});
}
// ✅ Bien: Cediendo entre chunks
async function procesarItemsCediendo(items) {
const TAMANO_CHUNK = 10;
for (let i = 0; i < items.length; i += TAMANO_CHUNK) {
const chunk = items.slice(i, i + TAMANO_CHUNK);
chunk.forEach(item => computacionPesada(item));
// Ceder al hilo principal
await new Promise(resolve => setTimeout(resolve, 0));
}
}
Usando requestIdleCallback
Para trabajo no crítico, usa requestIdleCallback para ejecutar durante tiempo inactivo del navegador:
function procesarEnTiempoInactivo(tareas) {
function procesarSiguiente(deadline) {
while (deadline.timeRemaining() > 0 && tareas.length > 0) {
const tarea = tareas.shift();
procesarTarea(tarea);
}
if (tareas.length > 0) {
requestIdleCallback(procesarSiguiente);
}
}
requestIdleCallback(procesarSiguiente);
}
Usando scheduler.yield() (Navegadores Modernos)
La nueva Scheduler API proporciona yield explícito:
async function procesarConScheduler(items) {
for (const item of items) {
computacionPesada(item);
// Ceder para permitir procesamiento de input del usuario
if ('scheduler' in window) {
await scheduler.yield();
}
}
}
Estrategia 2: Optimizar Carga de JavaScript
Code Splitting
Carga solo el JavaScript necesario para el renderizado inicial:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};
Imports Dinámicos
Difiere características no críticas:
// ❌ Mal: Importar todo al inicio
import { caracteristicaPesada } from './caracteristicaPesada';
// ✅ Bien: Import dinámico cuando se necesita
async function cargarCaracteristica() {
const { caracteristicaPesada } = await import('./caracteristicaPesada');
caracteristicaPesada();
}
// Solo cargar cuando el usuario interactúa
boton.addEventListener('click', cargarCaracteristica);
Atributos Defer y Async
<!-- Crítico: Cargar y ejecutar en orden -->
<script src="critico.js"></script>
<!-- Defer: Descargar en paralelo, ejecutar después del parsing HTML -->
<script src="app.js" defer></script>
<!-- Async: Descargar en paralelo, ejecutar cuando esté listo -->
<script src="analytics.js" async></script>
Estrategia 3: Usar Web Workers para Computación Pesada
Mueve trabajo intensivo de CPU fuera del hilo principal completamente:
Creando un Web Worker
// worker.js
self.onmessage = function(e) {
const resultado = computacionPesada(e.data);
self.postMessage(resultado);
};
function computacionPesada(datos) {
// Cálculos complejos aquí
return datosProcesados;
}
Usando el Worker
// main.js
const worker = new Worker('worker.js');
worker.onmessage = function(e) {
mostrarResultados(e.data);
};
// Enviar trabajo al worker - no bloquea hilo principal
worker.postMessage(conjuntoDatosGrande);
Cuándo Usar Web Workers
| Caso de Uso | ¿Web Worker? | Razón |
|---|---|---|
| Procesamiento de imágenes | ✅ Sí | CPU-intensivo, no necesita DOM |
| Parsing de datos (JSON) | ✅ Sí | Puede ser lento para archivos grandes |
| Ordenar arrays grandes | ✅ Sí | O(n log n) puede bloquear |
| Manipulación del DOM | ❌ No | Workers no pueden acceder al DOM |
| Cálculos pequeños | ❌ No | Overhead no vale la pena |
Estrategia 4: Optimizar Scripts de Terceros
Los scripts de terceros frecuentemente son los mayores contribuyentes al TBT.
Auditar Impacto de Terceros
Usa el panel de Performance de Chrome DevTools:
- Abre DevTools → pestaña Performance
- Graba la carga de la página
- Busca Long Tasks (esquinas rojas)
- Identifica qué scripts los causan
Lazy Load de Scripts de Terceros
// Cargar widget de chat solo cuando el usuario hace scroll cerca del footer
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
cargarWidgetChat();
observer.disconnect();
}
});
observer.observe(document.querySelector('#footer'));
function cargarWidgetChat() {
const script = document.createElement('script');
script.src = 'https://chat-widget.example.com/widget.js';
document.body.appendChild(script);
}
Usar Resource Hints
<!-- Establecer conexiones tempranas -->
<link rel="preconnect" href="https://analytics.example.com">
<link rel="dns-prefetch" href="https://ads.example.com">
Self-Host de Recursos Críticos de Terceros
<!-- En lugar de CDN externo -->
<script src="https://cdn.example.com/library.js"></script>
<!-- Self-host para mejor control -->
<script src="/js/vendor/library.js"></script>
Estrategia 5: Optimizar Aplicaciones React/Vue/Angular
Optimizaciones React
// Usar React.lazy para code splitting
const ComponentePesado = React.lazy(() => import('./ComponentePesado'));
function App() {
return (
<Suspense fallback={<Cargando />}>
<ComponentePesado />
</Suspense>
);
}
// Usar useDeferredValue para actualizaciones costosas
function ResultadosBusqueda({ query }) {
const queryDiferido = useDeferredValue(query);
return <ListaCostosa query={queryDiferido} />;
}
Optimizaciones Vue
// Componentes async
const ComponentePesado = defineAsyncComponent(() =>
import('./ComponentePesado.vue')
);
// Diferir actualizaciones reactivas
import { computed, shallowRef } from 'vue';
const datosGrandes = shallowRef(null);
const datosProcesados = computed(() => {
// Solo ejecuta cuando explícitamente se necesita
return procesoPesado(datosGrandes.value);
});
Optimizaciones Angular
// Lazy load de rutas
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module')
.then(m => m.DashboardModule)
}
];
// Usar OnPush change detection
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ComponenteOptimizado {}
Midiendo Mejoras de TBT
Panel de Performance de Chrome DevTools
- Abre DevTools (F12)
- Ve a la pestaña Performance
- Haz clic en Record, luego recarga la página
- Detén la grabación después de que cargue la página
- Busca:
- Long Tasks (tareas con esquinas rojas)
- Actividad del hilo principal (amarillo = JavaScript)
- Total blocking time en el resumen
Lighthouse CI
Automatiza el tracking de TBT en CI/CD:
# .github/workflows/lighthouse.yml
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v9
with:
urls: |
https://example.com/
budgetPath: ./budget.json
// budget.json
{
"timings": [
{
"metric": "total-blocking-time",
"budget": 200
}
]
}
Monitoreo de Usuarios Reales
Rastrea el proxy de TBT (Long Tasks) en producción:
// Observar Long Tasks
const observer = new PerformanceObserver((lista) => {
for (const entry of lista.getEntries()) {
if (entry.duration > 50) {
console.log('Long Task:', entry.duration, 'ms');
// Enviar a analytics
analytics.track('long_task', {
duracion: entry.duration,
tiempoInicio: entry.startTime
});
}
}
});
observer.observe({ entryTypes: ['longtask'] });
Errores Comunes y Soluciones
Error 1: Procesamiento de Datos Sincrónico
// ❌ Problema: Parsear JSON grande bloquea el hilo principal
const datos = JSON.parse(jsonGrande);
// ✅ Solución: Usar streaming o Web Worker
const worker = new Worker('json-parser-worker.js');
worker.postMessage(jsonGrande);
Error 2: Renders Costosos en React
// ❌ Problema: Re-renderizar componente costoso en cada cambio de estado
function Dashboard() {
const [filtro, setFiltro] = useState('');
return <GraficoCostoso datos={todosLosDatos} filtro={filtro} />;
}
// ✅ Solución: Memoizar componentes costosos
const GraficoMemoizado = React.memo(GraficoCostoso);
function Dashboard() {
const [filtro, setFiltro] = useState('');
const datosFiltrados = useMemo(() =>
filtrarDatos(todosLosDatos, filtro), [filtro]
);
return <GraficoMemoizado datos={datosFiltrados} />;
}
Error 3: Imágenes No Optimizadas Causando Layout
<!-- ❌ Problema: Decodificación de imagen grande bloquea hilo principal -->
<img src="imagen-enorme.jpg">
<!-- ✅ Solución: Usar decode() y dimensiones apropiadas -->
<img src="optimizada.webp"
width="800" height="600"
decoding="async"
loading="lazy">
Checklist de Optimización de TBT
Antes de desplegar, verifica:
- [ ] Ningún Long Task > 150ms durante carga de página
- [ ] Scripts de terceros cargados asincrónicamente
- [ ] Bundles de JavaScript bajo 200KB (comprimidos)
- [ ] Computaciones pesadas movidas a Web Workers
- [ ] Code splitting implementado para rutas/características
- [ ] React/Vue/Angular usando lazy loading
- [ ] TBT < 200ms en auditoría de Lighthouse
Artículos Relacionados
Continúa optimizando el rendimiento de tu sitio:
- TBT Explicado - Entiende los fundamentos de Total Blocking Time
- Guía de Optimización de INP - Mejora la interactividad real
- Guía de Optimización de LCP - Optimiza la carga de contenido
- Guía de Optimización de Speed Index - Mejora el progreso visual
📚 Volver al Hub de Performance SEO - Explora todos los temas de rendimiento
Referencias
- Chrome Developers - Total Blocking Time
- web.dev - Total Blocking Time (TBT)
- Chrome Developers - Lighthouse Performance Scoring
- web.dev - Optimize Long Tasks
Pruébalo Tú Mismo
¿Listo para optimizar tu TBT?
🔧 Descarga UXR SEO Analyzer (Gratis, análisis 100% local)
Disclaimer: Los analizadores en esta extensión son guías de referencia basadas en documentación oficial de Chrome Developers y web.dev. No representan verdades absolutas sobre cómo los motores de búsqueda evalúan tu contenido—solo los motores de búsqueda conocen sus algoritmos internos. Usa estas recomendaciones como punto de partida para mejorar tu sitio.
Última actualización: 14 de diciembre de 2025