MT Escritos

Un cine de partículas sin dependencias

Publicado — 8 min de lectura

Durante veinte años construí trabajo que nadie ve. Esta es la parte visible, desmontada con la misma disciplina.

El trabajo de infraestructura tiene una estética extraña: bien hecho, nadie lo nota. Una copia de seguridad corre veinte años en silencio, y un día deja de correr, y ese día todos aprenden que existe. Esa es la medida del trabajo invisible: solo se juzga cuando falla.

Este sitio es un intento de traer esa costumbre a la superficie. Lo que sigue no es adorno, es la cosa desmontada: qué hay, por qué está y cuánto cuesta.

Primero viene la restricción

Una sola regla lo decidió todo: ninguna petición externa.

No es un lema, es una cabecera de respuesta. La política de seguridad del sitio dice:

Content-Security-Policy: default-src 'self'; script-src 'self';
  style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:;
  connect-src 'self'; object-src 'none'; base-uri 'self';
  form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests

En el momento en que escribes default-src 'self', tus opciones se cierran. Nada de GSAP, nada de three.js, nada de Lenis, ni siquiera una tipografía traída de un CDN. ¿Quieres una biblioteca de animación? La escribirás. ¿Quieres suavizado de desplazamiento? Lo escribirás. ¿Quieres un motor de partículas? Sí, también.

La regla se demostró sola una vez, de forma inesperada. Cloudflare inyecta su propio script de analítica en el borde de la red. La política del sitio lo bloqueó:

Loading the script 'https://static.cloudflareinsights.com/beacon.min.js/…'
violates the following Content Security Policy directive: "script-src 'self'"

En un servidor local Lighthouse da 100 en Buenas prácticas; en vivo da 93. Esos siete puntos son exactamente esto. Relajar la política habría sido fácil. En cambio se apagó la baliza — porque una regla significa algo solo cuando también rige en el momento incómodo.

Dónde viven las partículas

Todo el movimiento de la página se dibuja en un solo <canvas> con WebGL2. Las posiciones y velocidades de las partículas no están en la CPU; viven en dos texturas, y en cada fotograma un sombreador de fragmentos las hace avanzar. El resultado se escribe en una textura y, en el fotograma siguiente, esa textura pasa a ser la entrada. El truco más viejo de la GPGPU: hacer el cálculo de textura a textura, nunca hacia la pantalla.

En la práctica esto significa que el número de partículas no le incumbe a la CPU. En escritorio el campo corre a densidad plena; en teléfono baja a 12.100 partículas — pero en ambos casos el trabajo por fotograma es la misma llamada al sombreador.

Mantener dos texturas es un detalle pequeño pero portante. No se puede leer y escribir la misma textura en una pasada; eso es comportamiento indefinido. Así que dos búferes se alternan: este fotograma lee A y escribe B, el siguiente intercambia los papeles. Este ping-pong es la higiene básica de la GPGPU.

El color es un cálculo aparte. La escena se dibuja en HDR, las zonas brillantes se extraen y se desenfocan (halo selectivo), y luego una curva ACES lo devuelve todo al rango mostrable. Esos tres pasos evitan el error más fácil del dibujo aditivo: que todo se queme hacia el blanco. La suite de verificación mide exactamente eso — la luma de pico por fotograma debe quedar bajo el umbral.

Las diferencias entre dispositivos las gobierna una escalera de cuatro peldaños:

PeldañoQué se apaga
0Nada — cine completo
1Sin desenfoque de zoom, halo a dos pasadas
2El número de partículas cae al 55%
3Repliegue a WebGL1 — GPGPU apagada del todo

El peldaño 3 importa: en un navegador sin WebGL2 la página no deja de funcionar, solo pierde la capa de simulación. Texto, navegación, contacto — todo sigue ahí.

Ocho idiomas, una sola fuente de verdad

El sitio sale en ocho idiomas: el turco en la raíz, los otros siete en sus propios directorios. Pero ocho páginas no se escriben a mano.

La página turca es el master. Cada nodo traducible lleva una marca:

<span data-i="about.e1t">Kurumsal altyapı</span>

Un generador lee esas marcas y extrae i18n/tr.json — hoy 102 claves. Los otros siete idiomas rellenan las mismas claves con su propio texto, y el generador imprime las páginas. Los campos traducibles del JSON-LD pasan por la misma tubería: cargo, ciudad, áreas de especialidad, descripción del servicio.

La regla más dura de este montaje: una clave que falta detiene la compilación. Sin repliegue, sin caer al inglés. Si un idioma se queda atrás, no sale nada. Mejor detenerse que publicar una página traducida a medias en silencio.

Que la página turca no se genere también es deliberado. El master se escribe a mano porque lleva comentarios de medición, márgenes de máscara y notas de alineación óptica — moverlos dentro de un generador los volvería ilegibles.

El resultado es medible: entre las ocho páginas los enlaces hreflang son recíprocos en 64 direcciones de 64, y x-default es correcto en las ocho.

Las tipografías son la parte más pesada

La página entera pesa 172 KB en once recursos. El reparto sorprende:

PartidaPeso
Tipografías (woff2)116 KB
JavaScript56 KB
Imágenes0 KB
CSS0 KB — en línea

No hay una sola <img> en el sitio. Toda la carga visual está en canvas y SVG. Frente a eso hay quince archivos woff2: cuatro familias para el latino y sustitutos aparte para el cirílico. Los archivos cirílicos están atados a los mismos nombres de familia mediante unicode-range — la página rusa nunca descarga un archivo latino; el navegador elige solo.

Las declaraciones @font-face van incrustadas en el HTML. Es una decisión medida: una hoja de estilos aparte era el único recurso que bloqueaba el renderizado, e incrustarla subió la puntuación móvil de Lighthouse de 99 a 100.

Transferencia real tras la compresión:

RecursoBrutoBrotliProporción
HTML87.890 B20.675 B76,5%
app.js186.011 B55.354 B70,2%

Sin medición nada se llama «hecho»

El repositorio guarda dos suites de verificación: 72 pruebas para la física de la escena y 100 para la traducción y la capa de descubrimiento. Ambas corren en un navegador real y ninguna mira una captura de pantalla — leen la luma del canvas, consultan canales de estado y comprueban umbrales con números.

La razón es simple: la verificación visual miente. Un panel puede desplazarse solo, una pestaña caer al fondo, requestAnimationFrame no haber latido nunca. La captura dice «funciona» cuando no ha empezado nada.

Cómo miente la medición

Lo que más tiempo llevó aquí no fue el código. Fue aprender a no fiarse de la medición misma.

Un ejemplo. Una página se abrió en un navegador de automatización y el contenido siguió invisible setenta segundos. Pantalla negra, el telón de apertura congelado. La lectura obvia: la página se ha colgado.

Se midieron tres cosas antes de sacar esa conclusión:

document.visibilityState  →  "hidden"
fotogramas rAF en 2 seg   →  0
avance de la apertura     →  0

La pestaña estaba en segundo plano. requestAnimationFrame no late en pestañas de fondo; la secuencia no había arrancado nunca. No había ningún fallo en la página — el fallo estaba en el entorno de medición.

Faltaba un paso para que aquello entrara en un informe como «hallazgo». El repositorio tiene una regla escrita para esto: no lo llames hecho sin medir — y mide también lo que crees haber visto. Una captura no es prueba; la prueba es la luma leída del canvas, o el propio canal de estado.

Dos averías silenciosas

Lo más interesante que saca a la luz una auditoría es aquello que no estaba buscando.

Primero: el campo lastmod del mapa del sitio leía su fecha del mapa del sitio anterior. El generador tomaba su propia salida como entrada. Una vez escrito, el valor nunca habría avanzado — por mucho que cambiara el contenido, el archivo mostraría siempre el mismo día.

Una señal de frescura congelada se convierte en mentira. Un lastmod rancio es peor que ninguno: le dice al rastreador que esta página no cambia. Ahora la fecha vive en un solo sitio, y el mapa la sigue.

Segundo: la lista de campos JSON-LD traducibles estaba duplicada en el generador en dos lugares — uno para la extracción, otro para la escritura. ¿Qué pasa si un campo nuevo se añade solo a uno? El campo no se traduce, en silencio. El texto turco se copia tal cual a los datos estructurados de siete páginas, y ninguna de las cien pruebas lo nota.

Era una trampa que habría roto justo lo que debía arreglar: quien añadiera el campo description que falta habría escrito turco en siete idiomas sin darse cuenta. Ahora la lista es una sola fuente.

La lección compartida: las averías más peligrosas de un sistema son aquellas en las que ninguna prueba falla.

El truco rechazado

La línea más honesta de este proyecto no está en un informe de errores. Está en un acta de decisión.

En móvil, Lighthouse no consigue producir un valor de Largest Contentful Paint para la portada. La causa es la secuencia de apertura: cuatro segundos de telón, luego la congelación en el contacto, luego el estallido, luego la transición de tinta. Antes de eso no hay texto del DOM visible, y Lighthouse cierra su traza mucho antes. Resultado: NO_LCP — la métrica no es calculable.

Se midió una corrección. Poniendo la opacidad inicial de la capa de contenido en 0.004 en vez de 0, el LCP cae de 9.052 ms a 164 ms, y el titular completo queda sellado como candidato a LCP. No hay coste visual: 0,004 de opacidad aporta como mucho 1/255 de unidad, es decir, nada.

Justo por eso se rechazó.

El LCP se define como el mayor bloque de texto visible en el área de visualización. Un titular al 0,004 de opacidad no es visible. La excepción opacity: 0 de Chromium existe para bloquear exactamente este patrón; 0,004 se cuela por su borde. El número mejoraría, la verdad no: el visitante seguiría viendo el titular nueve segundos después.

La decisión entró así en la documentación: no devolváis este patrón.

Por qué esta página es distinta

La página que estás leyendo no carga el motor descrito arriba. El campo de partículas del fondo es un script aparte y muy pequeño; el texto es plenamente visible en el primer pintado y no espera tras ninguna puerta de opacity.

La razón es la sección anterior. Un texto existe para ser leído y citado. La apertura cinematográfica es la firma de la portada, pero en una página de texto esa misma secuencia retrasaría no solo la medición sino la lectura. Esta sección tiene un presupuesto escrito — LCP por debajo de 2,5 segundos, JavaScript de página por debajo de 60 KB, ninguna puerta de opacidad sobre el contenido — y la suite de verificación lo comprueba en cada ejecución.

La misma disciplina, distinto resultado. Si la estética del trabajo invisible es que nunca falla, la estética del trabajo visible es que se puede medir.