wandres.dev
FLEXBOX: EL ALGORITMO · Base, crecer, encoger

El algoritmo de distribución, paso a paso

El bucle de resolución de longitudes flexibles tal y como lo ejecuta el motor: congelar inflexibles, repartir, recortar, contar violaciones y repetir. Con una traza completa sobre un caso real.

⏱ 21 min

Flexbox no reparte el espacio de una pasada. Ejecuta un bucle que reparte, comprueba si algún elemento se ha salido de sus límites, congela a los infractores y vuelve a empezar con lo que queda. Ese bucle está descrito en la sección 9.7 de la especificación, se llama resolución de longitudes flexibles, y saber ejecutarlo a mano es lo que convierte flexbox de un conjunto de recetas en un sistema que puedes predecir sin abrir el navegador.

🎯 Al terminar esta lección sabrás
  • Enumerar los pasos del bucle de resolución en orden.
  • Explicar por qué el algoritmo tiene que iterar en lugar de resolver de una vez.
  • Trazar a mano un caso con límites que se violan y llegar al resultado exacto.
  • Justificar por qué el bucle siempre termina.

Por qué hace falta un bucle

Repartir el espacio libre en proporción a unos factores es aritmética de una línea. El problema aparece cuando un elemento tiene max-inline-size o min-inline-size.

Supón que repartes y a un elemento le tocan 300px, pero su max-inline-size es 200px. No puede quedarse con 300, así que se queda con 200. Y ahora sobran 100px que ya habías repartido y que hay que volver a distribuir entre los demás. Ese nuevo reparto puede provocar que otro elemento supere su propio límite, y vuelta a empezar.

No hay forma de resolverlo de una sola pasada sin resolver un sistema con restricciones de desigualdad. La especificación elige un enfoque iterativo, más simple de implementar y de razonar: reparte, detecta infracciones, congela a los infractores en su límite y repite con el resto.

flowchart TB
A[Sumar los tamanos hipoteticos de todos los elementos] --> B[Comparar con el tamano del contenedor]
B --> C[Sobra espacio usar el factor grow]
B --> D[Falta espacio usar el factor shrink]
C --> E[Congelar los elementos inflexibles]
D --> E
E --> F[Calcular el espacio libre restante]
F --> G[Repartir entre los no congelados segun su factor]
G --> H[Recortar cada tamano con su min y su max]
H --> I[Sumar todas las violaciones]
I --> J[Suma cero congelar todos y terminar]
I --> K[Suma positiva congelar los que violaron el minimo]
I --> L[Suma negativa congelar los que violaron el maximo]
K --> F
L --> F
J --> M[Tamanos finales]
style A fill:#89b4fa,color:#11111b
style B fill:#89b4fa,color:#11111b
style C fill:#a6e3a1,color:#11111b
style D fill:#f9e2af,color:#11111b
style E fill:#89b4fa,color:#11111b
style F fill:#89b4fa,color:#11111b
style G fill:#cba6f7,color:#11111b
style H fill:#f9e2af,color:#11111b
style I fill:#cba6f7,color:#11111b
style J fill:#a6e3a1,color:#11111b
style K fill:#f38ba8,color:#11111b
style L fill:#f38ba8,color:#11111b
style M fill:#a6e3a1,color:#11111b

Los pasos, en su orden

Paso 1. Elegir la rama. Se suman los tamaños hipotéticos de todos los elementos, márgenes y gaps incluidos. Si la suma es menor que el tamaño interior del contenedor en el eje principal, se usará flex-grow; si no, flex-shrink. La decisión se toma una vez y no se revisa durante todo el bucle.

Paso 2. Congelar inflexibles. Se congelan de entrada los elementos con factor cero. Además, en la rama de crecimiento se congelan los que tienen la base mayor que su tamaño hipotético; en la rama de encogimiento, los que la tienen menor. Un elemento congelado ya tiene su tamaño final y no participa en nada más.

Paso 3. Espacio libre restante. Se recalcula: tamaño del contenedor menos la suma de las bases de los no congelados menos la suma de los tamaños ya fijados de los congelados.

Paso 4a. ¿Queda algo por hacer? Si todos están congelados, el bucle termina.

Paso 4b. Corregir factores menores que uno. Si la suma de los factores de los elementos no congelados es menor que 1, se multiplica el espacio libre inicial por esa suma, y si el resultado es menor en magnitud que el espacio libre restante, se usa ese valor. Es el mecanismo que hace que dos elementos con flex-grow: 0.25 repartan solo la mitad del sobrante.

Paso 4c. Repartir. En la rama de crecimiento, cada elemento recibe una parte proporcional a su flex-grow sobre la suma de los grows. En la de encogimiento, proporcional a su flex-shrink multiplicado por su base, sobre la suma de esos productos.

Paso 4d. Recortar y anotar. Se aplica a cada elemento su min y su max. Si el valor sube al chocar con el mínimo, se anota una violación positiva; si baja al chocar con el máximo, una violación negativa; si no cambia, cero.

Paso 4e. Congelar a los infractores. Se suman todas las violaciones. Si el total es cero, se congelan todos y se termina. Si es positivo, se congelan solo los que violaron el mínimo. Si es negativo, solo los que violaron el máximo.

Paso 4f. Volver al paso 3.

El paso 4e es el más sutil. La lógica es esta: si en conjunto se ha añadido tamaño por culpa de los mínimos, los que sobran son los que fueron empujados hacia arriba, y hay que congelarlos y redistribuir el déficit entre los demás. Congelar solo a los infractores del mismo signo evita oscilaciones infinitas.

Una traza completa

Contenedor de 500px, sin gaps. Tres elementos:

.contenedor { display: flex; inline-size: 500px; }
.a { flex: 1 1 100px; max-inline-size: 150px; }
.b { flex: 1 1 100px; }
.c { flex: 2 1 100px; }

Paso 1. Hipotéticos: 100 + 100 + 100 = 300. Contenedor: 500. Sobra, así que rama de crecimiento.

Paso 2. Ningún factor es cero, ninguna base supera su hipotético. No se congela nadie.

Paso 3. Espacio libre = 500 − 300 = 200.

Paso 4c. Suma de grows = 1 + 1 + 2 = 4. Reparto: a recibe 200 × 1/4 = 50, b recibe 50, c recibe 100. Tamaños tentativos: 150, 150, 200.

Paso 4d. a tiene max-inline-size: 150 y su tentativo es exactamente 150. No hay violación. Los otros dos no tienen límites. Total de violaciones: 0.

Paso 4e. Total cero: se congela todo y termina. Resultado: 150, 150, 200.

Cambiemos el máximo de a a 120px y repitamos desde el paso 4d.

Paso 4d, segunda versión. a tentativo 150, máximo 120: se recorta a 120 y se anota una violación de −30. Total: −30.

Paso 4e. Total negativo: se congelan los que violaron el máximo, es decir a, en 120px.

Paso 3, segunda vuelta. Espacio libre = 500 − 120 (congelado) − 100 − 100 (bases de los no congelados) = 180.

Paso 4c, segunda vuelta. Suma de grows de los no congelados = 1 + 2 = 3. b recibe 180 × 1/3 = 60, c recibe 120. Tentativos: 160 y 220.

Paso 4d. Sin límites, sin violaciones. Total cero.

Paso 4e. Se congela todo. Resultado final: 120, 160, 220. Suma: 500. Cuadra.

📝
El bucle siempre termina

Cada iteración congela al menos un elemento, y el número de elementos es finito. En el peor caso el bucle da tantas vueltas como elementos haya. No hay riesgo de bucle infinito ni de oscilación, y esa garantía es exactamente el motivo de que en el paso 4e solo se congelen los infractores de un signo.

Lo que el algoritmo no hace

Tres cosas quedan fuera de este bucle y conviene no atribuírselas.

No decide la altura de nada. El bucle opera solo en el eje principal; el eje transversal se resuelve después, con align-items y con la altura de la línea.

No coloca los elementos. El reparto del espacio sobrante después del bucle —cuando todos los elementos han acabado congelados y aún queda hueco porque nadie podía crecer más— lo hace justify-content. Por eso justify-content: space-between no tiene ningún efecto si algún elemento tiene flex-grow positivo y sin tope: no queda espacio que distribuir.

No reparte entre líneas. Con flex-wrap, primero se decide qué elementos van en cada línea y después se ejecuta este bucle por separado en cada línea. Ese es el motivo de que las líneas de un contenedor con wrap sean independientes entre sí.

Flexbox es un solucionador de restricciones, y por eso su comportamiento es emergente

Una vez que has trazado el bucle a mano, cambia la forma de leer flexbox. Las propiedades que escribes —flex-grow, flex-shrink, flex-basis, min-inline-size, max-inline-size— no son instrucciones que el motor ejecute en orden: son restricciones que alimentan un solucionador iterativo, y el resultado que ves es la solución de ese sistema, no la consecuencia directa de ninguna declaración concreta. Eso explica la propiedad más frustrante de depurar flexbox: no existe una relación uno a uno entre una declaración y un píxel. Cambias el max-inline-size de un elemento y se mueve otro que está al otro lado del contenedor, porque congelarlo antes alteró el espacio libre de las vueltas siguientes. Es comportamiento emergente en el sentido técnico del término, y no se depura leyendo una regla: se depura reconstruyendo el estado del sistema. La consecuencia práctica es un cambio de método. Ante un layout flex que no cuadra, deja de preguntarte qué propiedad está mal y empieza por listar, para cada elemento, sus cinco entradas: base, grow, shrink, mínimo y máximo. Con esa tabla delante, el resultado es aritmética y siempre lo has podido calcular. Sin ella, estás probando combinaciones a ciegas contra un solucionador que tiene más estado del que cabe en la cabeza. La misma lección se aplica tal cual a grid, que ejecuta un algoritmo análogo con tres fases sobre las pistas.