backdrop-filter: qué hace el navegador y por qué es caro
Las cuatro operaciones que ejecuta cada elemento con backdrop-filter en cada fotograma, qué es el backdrop root y por qué a veces no se ve nada, y qué pasa en una lista con veinte elementos.
backdrop-filter es la propiedad con peor relación entre lo que cuesta escribirla y lo que cuesta ejecutarla de todo CSS. Una línea produce el efecto de cristal esmerilado que lleva años de moda, y esa línea obliga al navegador a leer lo que ya ha pintado, cosa que ninguna otra propiedad hace y que rompe el modelo de trabajo de una GPU. En un elemento fijo es aceptable. En veinte elementos que se desplazan, no.
- Describir las cuatro operaciones de una pasada de
backdrop-filter. - Explicar por qué la lectura del búfer es la parte cara y no el desenfoque.
- Localizar el backdrop root y diagnosticar por qué un efecto no aparece.
- Elegir entre las tres estrategias para listas con muchos elementos.
Qué es el backdrop
El backdrop de un elemento es todo lo que se ha pintado debajo de él en el punto del orden de pintado en que le toca el turno. backdrop-filter aplica una cadena de filtros a esa imagen, la compone, y después pinta el elemento encima.
.barra {
position: sticky;
inset-block-start: 0;
background: color-mix(in oklab, Canvas 70%, transparent);
backdrop-filter: blur(16px) saturate(1.6);
}
Dos condiciones para que se vea algo. El elemento tiene que ser parcialmente transparente: si su fondo es opaco, tapa el backdrop filtrado y no notas nada. Y tiene que haber algo detrás: sobre el fondo liso de la página no hay nada que desenfocar.
El saturate() acompaña casi siempre al blur() y no es decorativo. Desenfocar promedia colores, y promediar colores baja la saturación; subirla compensa esa pérdida y es lo que hace que el cristal se vea vivo en vez de lechoso.
Las cuatro operaciones de cada pasada
Aquí está el motivo del coste, y conviene verlo por pasos.
flowchart TB
A[Se pinta todo lo que hay debajo del elemento] --> B[Se delimita la region del backdrop]
B --> C[Se copia esa region del buffer de destino a una textura]
C --> D[Se reduce la escala y se aplica la cadena de filtros]
D --> E[Se compone el resultado filtrado en su sitio]
E --> F[Se pinta el elemento encima]
F --> G{Queda otro elemento con backdrop-filter}
G -- Si --> B
G -- No --> H[Fotograma terminado]
style A fill:#89b4fa,color:#11111b
style C fill:#f38ba8,color:#11111b
style D fill:#f9e2af,color:#11111b
style H fill:#a6e3a1,color:#11111bEl paso rojo es el problema. Una GPU está diseñada para escribir píxeles en un búfer de destino y para leer de texturas que ya tenía preparadas. Leer del propio búfer en el que está escribiendo, a mitad de fotograma, es la operación que peor se le da: obliga a sincronizar, a resolver el trabajo pendiente y a copiar memoria.
En los renderizadores por tiles, que son los de prácticamente todos los móviles, es todavía peor. Esos chips trabajan acumulando comandos y resolviendo la escena tile a tile en memoria interna, precisamente para no tocar la memoria principal. Una lectura del framebuffer a mitad de la pasada les obliga a resolver el tile y volcarlo, que es exactamente lo que su arquitectura estaba diseñada para evitar.
El desenfoque en sí, en cambio, es barato: los navegadores reducen la escala de la textura antes de aplicarlo, con lo que un radio grande no cuesta mucho más que uno pequeño. La parte cara es la copia, no el filtro.
Y la copia no se puede cachear. Un filter normal se aplica sobre el elemento, así que mientras el elemento no cambie el resultado sirve para el fotograma siguiente. Un backdrop-filter depende de lo que hay detrás, y en cuanto algo detrás se mueve —un scroll, una animación, un vídeo— el resultado queda invalidado. En una página que se desplaza, eso es cada fotograma.
El backdrop root
La segunda parte de la especificación que hay que conocer, porque es la causa del noventa por ciento de los “no funciona”.
El backdrop de un elemento no es toda la página: es todo lo pintado por debajo hasta el ancestro más cercano que forme un backdrop root. Y forman backdrop root, entre otros, los elementos con filter, con opacity menor que 1, con mask, con mix-blend-mode distinto de normal, con isolation: isolate, con will-change de cualquiera de esos, y el elemento raíz del documento.
La consecuencia práctica:
<div class="panel">
<div class="cristal">Contenido</div>
</div>
/* Esto rompe el efecto del hijo. */
.panel { opacity: 0.99; }
.cristal { backdrop-filter: blur(16px); }
Con esa opacidad, .panel se convierte en backdrop root. El backdrop de .cristal pasa a ser únicamente lo que .panel haya pintado debajo de él, que probablemente es nada, y el desenfoque no tiene sobre qué actuar. El síntoma es que el efecto desaparece entero, no que se atenúe.
La lista de sospechosos cuando esto pasa: un opacity de 0.99 puesto para forzar una capa, un transform: translateZ(0), un filter: blur(0), un will-change cualquiera, un overflow con contain. Todos son cosas que la gente añade por rendimiento y que aquí destruyen el efecto.
El coste en una lista
Ahora el escenario que la rama de retorno del diagrama anticipaba. Veinte tarjetas con backdrop-filter en una lista que se desplaza.
Las pasadas no se pueden agrupar ni paralelizar, y la razón es de orden: el backdrop de la tarjeta número tres incluye todo lo pintado antes, incluidas las tarjetas uno y dos ya compuestas con sus propios backdrops filtrados. Hay una dependencia secuencial estricta. El navegador tiene que hacer copia, filtro, composición y pintado de la primera antes de poder empezar la región de la segunda.
Veinte elementos son veinte lecturas del búfer, veinte filtros y veinte composiciones por fotograma, con una barrera de sincronización entre cada dos. A sesenta fotogramas por segundo son mil doscientas lecturas por segundo. En un portátil se nota; en un móvil de gama media la lista deja de seguir al dedo.
Tres estrategias, en orden de eficacia.
Un elemento en lugar de veinte. Si el efecto de cristal es de una barra, una cabecera o un panel, ponlo en ese único elemento. Veinte chips esmerilados no aportan veinte veces más que uno, y cuestan veinte veces más.
Sacarlo del contenido que se desplaza. Un backdrop-filter en un elemento sticky o fixed sigue costando su pasada por fotograma, pero es una pasada, y su región es pequeña y constante.
Degradar cuando no toca. El efecto es puramente estético, así que un fondo semitransparente sólido sustituye bien:
.cristal {
background: color-mix(in oklab, Canvas 88%, transparent);
}
@supports (backdrop-filter: blur(1px)) {
@media (prefers-reduced-transparency: no-preference) {
.cristal {
background: color-mix(in oklab, Canvas 65%, transparent);
backdrop-filter: blur(16px) saturate(1.6);
}
}
}
prefers-reduced-transparency es una consulta reciente que refleja el ajuste del sistema operativo para reducir efectos de transparencia. Respetarla es gratis y quita el efecto justo en los dispositivos y usuarios donde más molesta.
Los píxeles del borde de la región no tienen vecinos fuera de ella que promediar, así que el desenfoque los trata con los que hay, y el resultado es una franja de uno o dos píxeles con menos desenfoque del que debería. En un panel con esquinas redondeadas y contenido de alto contraste detrás, esa franja se ve como un halo. La atenuación habitual es un border de un píxel semitransparente que la tape.
Todas las peculiaridades de esta propiedad —el coste, la imposibilidad de cachear, el backdrop root, la serialización— salen de un único hecho estructural: backdrop-filter es la única propiedad de CSS que crea una dependencia de un elemento hacia lo que hay por debajo de él. El modelo de renderizado entero de la web está construido sobre la suposición contraria: cada elemento sabe pintarse por su cuenta, el resultado se compone en un árbol, y el compositor combina texturas independientes que puede rasterizar en el orden que quiera, en paralelo, en hilos distintos, y reutilizar entre fotogramas mientras el elemento no cambie. Esa independencia es lo que permite que un scroll sea barato: las capas ya están rasterizadas y solo hay que moverlas. backdrop-filter introduce una arista en ese grafo que va en el sentido prohibido, y con ella se caen las cuatro propiedades que hacían el sistema rápido. Ya no se puede paralelizar, porque hay orden. Ya no se puede cachear, porque la entrada depende de vecinos que se mueven. Ya no se puede componer en cualquier orden, porque el resultado de uno es la entrada del siguiente. Y ya no se puede resolver todo en el compositor, porque hay una lectura del destino a mitad del proceso. Nada de eso es un defecto de implementación que vaya a mejorar con el tiempo: es la consecuencia matemática de haber pedido una dependencia hacia atrás. De ahí sale el criterio para usarla que aguanta cualquier revisión de rendimiento: cuenta el número de elementos con backdrop-filter que pueden estar visibles a la vez y multiplícalo por si hay movimiento detrás. Cero movimiento y un elemento es gratis. Un elemento con scroll detrás es asumible y es el caso de la barra pegajosa, que es donde el efecto nació. Dos o tres es el límite razonable. Diez, con o sin movimiento, significa que has elegido un efecto visual que la arquitectura de renderizado de la web no sabe hacer barato, y ninguna optimización lo va a cambiar.
- Pon una barra pegajosa con
backdrop-filtery graba un perfil mientras haces scroll. - Añade
opacity: 0.99a un ancestro y observa cómo desaparece el efecto. - Repite el efecto en veinte tarjetas de una lista y compara los tiempos de fotograma.
- Sustituye las veinte por un solo panel y vuelve a medir.
- Activa la reducción de transparencia del sistema y comprueba que tu fallback entra.