Skip to content

La regla de los 14 KB: un portfolio en una sola roundtrip de TCP

TCP arranca una conexión enviando alrededor de 14 KB antes de pausar a esperar un ACK. Mete tu página entera en esa ventana y aterriza en una sola roundtrip. Acá cómo lo hace el home de este sitio.

Performance APITCPAstroGitHub Actions

El Live Proof strip del home se estabiliza en 14.0 KB de bytes totales transferidos para HTML, CSS y JS combinados. No es un número redondo elegido por estética. Es el presupuesto que tienes que respetar si quieres que la página llegue en una sola roundtrip de TCP.

Por qué 14 KB

Cuando una conexión TCP se abre, el servidor no manda todo lo que tiene. Manda una primera ráfaga corta y después espera el ACK del cliente antes de mandar más. Esto protege a la red de un sender que aún no sabe cuánto ancho de banda está disponible.

El tamaño de esa primera ráfaga se llama initial congestion window (initcwnd). Desde el RFC 6928 (publicado en 2013), Linux y los principales servidores web usan un initcwnd default de 10 segmentos. El Maximum Segment Size en la mayoría de los paths es de unos 1460 bytes, lo que da:

10 segmentos × 1460 bytes = 14,600 bytes ≈ 14.3 KB

El overhead de headers se come un pedazo chico, así que una regla útil es “los primeros ~14 KB de HTML / CSS / JS llegan en una roundtrip; los bytes siguientes esperan una segunda roundtrip”.

Esa segunda roundtrip cuesta lo que cueste el round-trip time entre tu visitante y tu edge. Para un deploy en Vercel, eso va desde 20 ms en la misma región hasta 300 ms al otro lado del mundo. Quedarse abajo de 14 KB te ahorra esa roundtrip que cualquier visitante pagaría en el primer paint.

Cómo se ve el slow-start

   Cliente                               Servidor
     │                                     │
     │  ─── SYN ───────────────────────▶   │
     │  ◀──────────────── SYN/ACK ──────   │   (1 RTT, setup)
     │  ─── ACK + GET / ──────────────▶    │
     │                                     │
     │  ◀── 10 segmentos (~14 KB) ──────   │   (1 RTT, primera ráfaga)
     │  ─── ACK ──────────────────────▶    │
     │                                     │
     │  ◀── más segmentos ──────────────   │   (2da RTT y siguientes)
     │                                     │

Si tu página entra en esa primera ráfaga, el browser puede empezar a parsear y renderizar de inmediato. Si no, los bytes siguientes quedan detrás de una roundtrip que no puedes hacer más rápida.

Cómo este sitio se mantiene abajo de 14 KB

Rediseñé este sitio en junio de 2026 y lo migré a Astro 7 + Tailwind 4 + TypeScript 6. Dos pasadas importaron para el presupuesto:

1. Sacar dependencias cuyo valor no pagaba sus bytes. Removí nueve paquetes. El peso aproximado de cada uno en una página típica donde estaba cargado:

lenis            → ~9 KB minificado + brotli
motion           → ~22 KB
astro-icon       → ~5 KB (más payload SVG por icono)
@iconify-json/mdi → ~3 KB
astro-i18next    → ~6 KB
i18next          → ~14 KB
toggle dark-mode + CSS → ~3 KB
glass-card CSS (legacy) → ~2 KB
script inline de theme detection → ~1 KB

Cerca de 65 KB de JS y CSS fuera, nada visible en el rediseño.

2. Dejar que Tailwind 4 se pode solo. La config CSS-first de Tailwind 4 emite únicamente las utilities efectivamente referenciadas en source. La hoja generada del home pesa alrededor de 5 KB. No hay tailwind.config.ts ni overhead de JIT compilation en runtime.

3. Inline lo que se pueda. El build.inlineStylesheets: 'auto' de Astro mete los stylesheets chicos dentro del HTML en lugar de pagar un request aparte. Los íconos SVG self-hosted viajan dentro del payload HTML. Google Fonts se preconnectan y los font files cargan en paralelo; no cuentan contra la primera roundtrip porque son subresources.

Medirlo en cualquier página

Pega este script al final de tu página y mira tu propio presupuesto:

<script is:inline>
  (function () {
    function fmtKb(b) { return b < 1024 ? Math.round(b) + ' B' : (b / 1024).toFixed(1) + ' KB'; }
    function fmtMs(ms) { return ms < 1000 ? Math.round(ms) + ' ms' : (ms / 1000).toFixed(2) + ' s'; }

    window.addEventListener('load', function () {
      setTimeout(function () {
        var nav = performance.getEntriesByType('navigation')[0];
        var total = nav && nav.transferSize ? nav.transferSize : 0;
        performance.getEntriesByType('resource').forEach(function (r) {
          if (r.transferSize) total += r.transferSize;
        });
        console.log('peso página:', fmtKb(total));
        console.log('domInteractive:', fmtMs(nav.domInteractive));
      }, 250);
    });
  })();
</script>

transferSize ya considera la compresión. Si estás arriba del presupuesto, la barra del network panel de DevTools te dice qué recurso se está comiendo más.

Hacer cumplir el presupuesto en CI

Una vez que estás abajo de 14 KB, la única manera de quedarte ahí es hacer fallar el build cuando pasas la línea. Un GitHub Action mínimo para un build estático de Astro:

name: Page weight budget
on: [push, pull_request]

jobs:
  budget:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci && npm run build
      - name: Check homepage weight
        run: |
          BUDGET_BYTES=$((14 * 1024))
          PAGE=$(stat -c %s dist/index.html)
          CSS=$(find dist/_astro -name '*.css' -exec stat -c %s {} + | awk '{s+=$1} END {print s+0}')
          JS=$(find dist/_astro -name '*.js' -exec stat -c %s {} + | awk '{s+=$1} END {print s+0}')
          TOTAL=$((PAGE + CSS + JS))
          echo "homepage payload: $TOTAL bytes (budget: $BUDGET_BYTES)"
          if [ $TOTAL -gt $BUDGET_BYTES ]; then
            echo "::error::page payload exceeds 14 KB budget"
            exit 1
          fi

Corre antes del deploy. Si un PR mete una dependencia que empuja al home arriba de la línea, el merge queda bloqueado.

Caveats que vale conocer

  • El número 14 KB aplica a la primera conexión TCP a tu origin. Los subresources que se buscan sobre conexiones ya calientes no pagan la misma penalidad.
  • HTTP/2 y HTTP/3 multiplexan requests sobre una sola conexión, pero respetan el slow-start de TCP en la conexión inicial.
  • Los CDNs con conexiones persistentes a clientes (una fracción de visitantes recurrentes) se saltean el slow-start enteramente. La regla pesa más en visitantes nuevos y cargas con caché frío.
  • La compresión importa. Los 14 KB son “bytes en el cable”, es decir post gzip/brotli. El mismo sitio sin comprimir podría tener 50 KB y aun así entrar.

En síntesis

Este es el tipo de optimización que no aparece en un Lighthouse score hasta que todo lo demás ya está hecho. Si todavía no elegiste tipografía, sistema de color o fonts, primero eso. Pero una vez que el diseño está fijo, la regla de los 14 KB es de las wins más baratas en performance. Borras bytes, no agregas.

El home entero de este sitio está abajo del presupuesto ahora mismo: 14.0 KB transferidos, 160 ms de domInteractive en un browser desktop. El Live Proof strip lo mide en cada page load. Si en algún momento cruza la línea, el CI falla antes de que lo veas.


Más notas perf-nerd y el rant ocasional sobre Shopify Plus en X: @jaimesolis. Si quieres un build con presupuesto activo en tu proyecto, el formulario de contacto está a dos clicks.