Etiquetas

, ,

¿Cansado de esperar más de 5 segundos para que se visualice la página de un blog? ¿agobiado de esos sitios con decenas de widgets, gadgets, efectos AJAX y mashups que tardan horas en cargarse? ¿amargado porque has desarrollado un programa muy eficiente para el último framework de moda y «va lento»? Yo también, y me molesta mucho, considero que son chapuzas increíbles que no tienen en cuenta lo básico de la usabilidad e interfaces humanas: el tiempo de respuesta que percibe el usuario.

De Menéame nos podrán criticar todo, salvo que sea lento o que no hayamos tenido en cuenta este aspecto tan importante, por eso cuento unas pocas reglas que hemos seguido muy estrictamente. Algunas las conocíamos, otras hemos aprendido durante estos casi cinco años de desarrollo.

Hay muchos parámetros a tener en cuenta para desarrollar sitios web que sean «rápidos». No sólo hay que tener en cuenta la velocidad de los servidores, o el tiempo que el servidor en generar un HTML dinámico, hay muchos otros parámetros que afectan directamente al navegador y la percepción de los usuarios.

En julio de 2001 escribí un artículo en Bulma donde expliqué, basado en la experiencia y mediciones durante el desarrollo de los primeros sitios de Diari de BalearsÚltima Hora (en los años 1997-1998), los parámetros o mediciones técnicas fundamentales a tener en cuenta: tiempo de respuesta, tiempo de retorno, tiempo de descarga y el «tiempo para la visualización». Este último parámetro, el de visualización, es el que más efecto tiene para el usuario. Éste espera una respuesta rápida, esa «respuesta» se suele percibir por el tiempo que tarda en empezar mostrarse la página en el navegador.

En el artículo comento con ejemplos como se puede hacer que el tiempo de visualización sea mucho menor que el tiempo total necesario para generar y descargar el HTML completo mediante una maquetación optimizada. Así, una página maquetada sin tener en cuenta estos parámetros toma, de acuerdo con las mediciones de esos años, seis segundos para ser visualizadas.

Tvis = Tdescarga = 0.5 seg + 40 KB/7.5KBseg = 0.5 seg + 5.33 seg =~ 6 segundos

En cambio la misma página, pero formateada adecuadamente puede reducir el tiempo de visualización a un segundo.

Tvis = 0.5 seg + 4 KB/7.5KBseg = 0.5 seg + 0.53 seg =1 segundo

Esas mediciones se hicieron cuando reinaban las tablas, casi no se usaba CSS ni Javascript. Aún así las cosas han empeorado, incluso con el espectacular aumento de ancho de banda (lo mejor que había en 2001 era el RDSI a 64 kbps y muy pocas conexiones de 128 o 256 kbps), navegadores y potencia de cálculo hay muchos sitios populares que tardan más de seis segundos en visualizar la página.

Los problemas siguen siendo los mismos, aunque más complejos por esa influencia de los CSS, Javascripts, publicidad, librerías, efectos etc. Sería largo de explicar todas las recetas, pero aquí van algunas de las reglas que seguimos estrictamente en Menéame:

1. Minimizar las inclusión de CSS y Javascript

Los CSS y la mayoría de ficheros de Javascript se indican al principio del HTML y son bloqueantes, i.e. hacen que el navegador detenga todo el proceso de «dibujado» hasta que se hayan bajados todos los ficheros. Piensa mucho bien antes de incluir alegremente tantos ficheros CSS y Javascript en tu página, especialmente estos últimos. No es gratuito, todo lo contrario, estarás molestando mucho a tus lectores o usuarios. La regla fundamental es: minimiza el número de CSS y Javascript, no incluyas librerías que no necesitarás, minimiza el número de imágenes dentro del CSS (que generan conexiones adicionales).

Es muy común que blogs y sitios webs incluyan todo tipo de javascript externo (los widgets) sin tener en cuenta la penalización temporal que ejercen sobre su página. Estos javascripts bloquean completamente y se suele notar, especialmente si no fueron desarrollados cuidadosamente para evitar que el bloqueo sea prologando. Los buenos scripts suelen ser un pequeño código que sólo define un iframe de tamaño fijo y hace que éste cargue a posteriori el resto de javascript (con las consultas necesarias). Un ejemplo de buenos scripts en este sentido son los de Google AdSense.

Así que si no tienes más remedio que incluir javascript externo fíjate cómo está desarrollado, descarta aquellos que bloquean el dibujado de la página mientras hacen sus consultas a servidores externos. Si no te queda más remedio que utilizar esos script bloqueantes y lentos, tómate el trabajo para evitar el problema. Es lo que hicimos en el Menéame en los dos bloques de publicidad que se ven.

La publicidad se genera a partir de ficheros javascript del servidor de Social Media S.L. Este servidor verifica primero si hay alguna publicidad contratada para nuestro sitio de sus propias bases de datos y de bases de datos de terceros. Si no es así redirecciona para que se carguen los anuncios de AdSense. Todo este proceso es bastante lento y el API del servidor no está optimizado. La solución que usamos y se puede aplicar para cualquier sitio con un problema similar:

No se carga directamente al Javascript, sino que definimos un iframe de tamaño fijo y un html independiente que se carga de nuestros servidores. El html de ese iframe es el que incluye los javascript de publicidad. La carga del contenido del iframe no bloquea la página y tiene menos prioridad. Esta es la razón de que si váis al Menéame la publicidad se suele muestra después que se haya dibujado la página completa (probadlo y veréis el efecto).

Ya sabéis el truco que hay que hacer para solucionar esos widgets lentos, tómate el trabajo, tus lectores te lo agradecerán. Pero aún más importante: si eres desarrollador de widgets o plugins haz lo mismo en tu scripts. Aquí tienes un ejemplo de cómo lo hacemos para el «contador de votos» de Menéame check_url.js o este otro similar pero más elaborado.

Nota: está el atributo defer de la etiqueta script, pero usadlo con cuidado, tuve muchos problemas cuando lo intenté usar.

2. Retrasar la carga de Javascript que no sea necesaria para mostrar la página

Como comentaba en el punto anterior, hay que intentar minimizar el número de ficheros javascript que se incluyen al principio de la página. Cuando hay que cargar ficheros adicionales lo mejor es ponerlos al final del html, justo antes de la etiqueta </body>.

A veces no es simple porque esos javascript se pueden necesitar para mostrar contenido en la misma página. Eso se puede solucionar, como hacemos en el caso de noticias que tienen geolocalización y el mapa de Google Maps se muestra en la barra lateral, por ejemplo en esta noticia. La intención es retrasar lo máximo posible la carga del Javascript adicional de Menéame y el de Google Maps.

Podéis ver que el código que lo hace está al final del html, justo antes del </body> (el primer script incluye el API de Google, el segundo las funciones propias de Menéame):

<script  type="text/javascript"
  src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=ABQIAAAAocztgtKfY7lNdoKmmVwCrRTm04PVUmrAy_OEGIhbf1bFTbg4wRQr-dfRhZoi0UaKDoqFRXUZXOgLuw">
</script>
<script type="text/javascript"
  src="http://aws.mnmstatic.net/js/geo.js">
</script>

Al principio del HTML insertamos una llamada a geo_coder_load (definida en geo.js que se cargará al final) pero que se llamará después que se haya cargado todo el contenido usando la función $.() de Jquery:

<script type="text/javascript">
$(function(){geo_coder_load(34.0104402,-118.4983353, 5, 'published');})
</script>

En el lugar donde queremos que se inserte el mapa sólo definimos un div con las medidas deseadas:

<div id="map" style="width:300px;height:200px;margin-bottom:25px;">&nbsp;</div>

Lo que pasará es que la página se cargará y dibujará completamente, sólo entonces se llamará a la función geo_coder_load que llamará a las funciones del API de Google Maps para que dibuje el map en el div con id «map». Así evitamos que se bloquee la página mientras se carga el «inmenso» mapas (por eso, al igual que la publicidad, el mapa se muestra con «retraso»).

3. Maquetar de forma que se pueda «dibujar» parte de la página aunque no se haya bajado todo el HTML

En el artículo de Bulma de hace casi diez años comentaba el mismo tema, aunque orientado más a tablas los principios son los mismos:

  • Separar en módulos que puedan ser dibujados inmediatamente, por ejemplo la cabecera.
  • Definir todas las dimensiones de imágenes, áreas (por ejemplo el alto de la cabecera, el ancho de las columnas, etc.) y espacios:

En Menéame el ancho de completo ya se define en el CSS:

#wrap {
min-width: 952px;
max-width: 1200px;
margin: 0 auto;
...
}

#header{
height: 36px;
...
}

#naviwrap li{
width: 120px;
height: 20px;
..
}

#sidebar {
float: right;
width: 300px;
...
}

En cuanto el navegador interpreta el CSS de esas tres divs ya puede calcular las dimensiones necesarias para poder dibujar rápidamente: el ancho que tendrá la parte visible, el alto de la cabecera superior, el alto del menú de la izquierda y el ancho de la columna de la derecha (y por lo tanto ya calcula el ancho de la columna principal de contenido).

La parte que toma más tiempo de CPU en la portada o una noticia de Menéame es la consulta a los enlaces o los comentarios. Para disminuir aún más el tiempo de visualización lo primero que se envía al navegador es el contenido de la columna derecha (el #sidebar), como el ancho ya está especificado puede mostrarla aunque no se haya recibido el HTML de la columna principal. Por eso en conexiones u ordenadores lentos veréis que se empieza dibujando por la columna de la derecha.

4. Utilizar dominios diferentes para el contenido estático

Los navegadores (o al menos la mayoría) paralelizan las descargas de sitios diferentes al original, por eso es mejor que dichos ficheros se bajen de un dominio diferente.

<link rel="stylesheet" type="text/css" media="all" href="http://aws.mnmstatic.net/css/es/mnm65.css"/>
...
<link rel="shortcut icon" href="http://aws.mnmstatic.net/img/favicons/favicon4.ico" type="image/x-icon"/>
...
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js" type="text/javascript"></script>
...
<meta name="thumbnail_url" content="http://aws.mnmstatic.net/img/mnm/eli_thumbnail.png"/>
...
<img src="http://aws.mnmstatic.net/img/common/search-left-04.png" width="6" height="22" alt=""/>
...
<img src="http://aws.mnmstatic.net/cache/00/01/1-1280021590-20.jpg" width="20" height="20" alt="gallir"/>

La línea 3 es interesante. Hay librerías, como las JQuery, que son usadas por muchísimos sitios. Aunque se trata siempre del mismo código cada sitio incluye su propio fichero, lo que obliga al navegador a bajar una y otra vez lo mismo. Para evitarlo Google puso a disposición el servidor Google Apis, donde mantiene una copia de esas librerías para que puedan ser usadas por cualquier sitio. Si todos los sitios importantes los usasen se ahorraría mucho tiempo de los usuarios (además los servidores de Google suelen estar distribuidos y se bajan de sitios con la menor demora).

5. Usar dominios diferentes, no subdominios, para contenido estático

Esta apartado podría ser un subapartado del anterior, incluso el mismo, pero lo puse por separado ya que es un truco muy bueno para sitios complejos (con uso de cookies) y muchas imágenes o ficheros estáticos.

En el ejemplo anterior se puede ver que no usamos un subdominio de meneame.net (por ejemplo static.meneame.net) para servir el contenido estático, sino uno completamente diferente. El objetivo evitar agregar tráfico adicional por los cookies que se usan.

Cada cookie que se define en un dominio implica que por cada conexión el navegador envíe al servidor el valor de esos cookies. Esto pasa incluso para bajarse unas imágenes pequeñas de pocos bytes. Además el ancho de banda de subida suele ser bastante inferior al de bajada. La situación se agrava aún más con los cookies que definen las herramientas de estadísticas como Google Analytics.

La forma de evitar ese tráfico inútil para el contenido estático, lo mejor es usar un dominio completamente diferente. Cuando implementamos esto en Menéame hicimos las pruebas y ahorrábamos 14 KB de tráfico para un navegador con la cache vacía.

6. Definir tiempo de caducidad para el contenido estático

Para evitar que los navegadores se conecten cada vez para verificar si un fichero estático fue modificado es mejor definirles un tiempo de caducidad bastante elevado. De esta forma el navegador no volverá a verificar ese fichero hasta que haya pasado ese tiempo.

En menéame usamos 10 días, en el nginx:

location ~* \.(gif|jpg|png|js|css|html|ico)$ {
  expires   10d;
  ...
}

Por ejemplo para una imagen generará las siguientes cabeceras HTTP:

  Cache-Control: max-age=864000
  Expires: Mon, 06 Sep 2010 16:12:01 GMT

7. Comprimir el contenido de texto

Si un sitio está razonablemente bien programado, lo que más tiempo consume es la transmisión del HTML por la red, por eso los navegadores aceptan la versión comprimida. Con la potencia actual de los procesadores es mejor comprirlos antes de enviarlos, los servidores pueden hacerlo automáticamente y on-the-fly si el navegador lo acepta.

Por ejemplo la configuración del nginx para Menéame para comprimir en gzip:

    gzip  on;
    gzip_comp_level 4;
    gzip_proxied any;
    gzip_min_length 512;
    gzip_disable "MSIE [1-6]\.(?!.*SV1)";
    gzip_http_version 1.0;
    gzip_vary on;
    gzip_types text/css text/xml application/x-javascript application/atom+xml text/plain application/json application/xml+rss text/javascript;

Hay más reglas y trucos para minimizar los tiempos, por ejemplo poner todas las imágenes del CSS en una sola y tratarlas como «sprites» [*], pero si respetas la mayoría de estas 7 reglas la mejora de velocidad de tu sitio será muy notable.

[*] Particularmente no me gusta esta opción para sitios donde el diseño es dinámico con el Menéame. Cada vez que se agregan o modifican iconos obliga a generar una nueva imagen, que además debe ser compatible en sus coordenadas con la anterior, caso contrario hay que redefinirlas en todo el CSS. Es mucho tabajo de «administración» y sujeta a errores. Pero sí la veo como buena medida para los plugins o widgets que incluyen iconos y serán usados por muchos sitios (por lo que la «estabilidad» es mucho más prolongada). Más o menos lo mismo pienso sobre minimizar los javascripts.

¡No necesitas servidores más rápidos! debes intercalar

Esta sección es más friki-informática, y es la que en realidad me motivó a escribir este apunte. Pero por esas cosas de «completitud» quedó última y la más breve.

La tendencia actual de programación web es de usar plantillas cada vez más sofisticadas y complejas. Ejemplos típicos son la de generar todas las consultas a la base de datos y luego generar los resultados HTML en un bucle FOR dentro de la plantilla. Ejemplos más sofisticados son la de incluir plantillas dentro de otras o usar las características de herencia (o bloques). Algo similar ocurre con casos más sencillos, como usar el output buffer del PHP.

La mitología informática dice que así se mejora la negociación TCP/IP y se minimizan los paquetes enviados. Posiblemente sea verdad para sitios muy sencillos por con contenido tan simple que se genera muy rápido. Pero la realidad es que los sitios actuales generan páginas de varias decenas de KB (el HTML de la portada de Menéame tiene una media de unos 50-60 KB, una noticia con muchos comentarios puede llegar fácilmente a los 200 KB).

Hace pocas semanas liberamos Haanga, el sistema de plantillas de Menéame. Aunque éste permite la herencia no usamos esa características. Ni usamos el output buffer de PHP.  Tampoco usamos los grandes bucles FOR, sino que el contenido se genera progresivamente para cada noticia,  comentario o nota. Esta generación progresiva del HTML se usa para todos las partes de la página, para la cabecera, el pie, etc.

La razón es muy sencilla, así reducimos el tiempo de respuesta, el de retorno… y si se respetan las reglas comentadas anterioremente, el de visualización.

Un programa web tiene varias partes bien diferentes: consultas a la base de datos, lógica, generación de HTML, envío por la red. Para simplificar podemos considerar:

B: Consultas a base de datos y lógica:

H: Generación de HTML.

E: Envío a través de la red.

A: Ficheros adicionales (CSS, Javascript) que necesita bajar el navegador.

Así un patrón típico cuando se usa buffer del PHP, o se genera el HTML vía un template al final de la lógica sería (en azúl y sólo como referencia el momento en el que el navegador puede empezar a mostrar contenido):


BBBBBBBBBBHHHHHEEAAAEEEEEEEEEEEEEEEEEE

En la figura tenemos:

  • diez «unidades de tiempo» (llamémosles «tics») para la lógica y consultas a la base de datos,
  • cinco tics para la generación del HTML,
  • veinte tics para la compresión (si está habilitada) y descarga por la red y
  • tres tics para descargar ficheros adicionales.

Por supuesto es sólo un dibujo aproximado, si se usan plantillas más complicadas la generación de HTML podría tomar más tiempo que la lógica y consulta de base de datos. O viceversa si el HTML es simple pero las consultas son complicadas.

En el ejemplo el tiempo total que toma para bajar todo el HTML es de 38 tics, con suerte el usuario podrá empezar a visualizar la página web a los 20 tics.

Si el web es muy lento, la mayoría de programadores decidirán que lo mejor es poner un servidor de base de datos o web que sea más rápido, o que permita disminuir la carga y por lo tanto se reducirían los tiempos de base de datos y generación de HTML. Pero no es tan fácil, sobre todo porque la conexión al servidor de base de datos involucra –al menos para sitios grandes– conexiones vía red. Aunque se aumente la velocidad del procesador, el tiempo de latencia de la red sigue siendo el mismo.

Aún así, supongamos que cambia por servidores y redes más potentes y logra reducir casi a la mitad [*] el tiempo de base de datos y generación de HTML:


BBBBBHHHEEAAAEEEEEEEEEEEEEEEEEE

Después de la inversión para conseguir semejante mejor y el trabajo de migrar a una nueva infraestructura habrá reducido el tiempo total a 31 tics (18 % de mejora) y el tiempo de visualización a 13 tics (35% de mejora).

[*] Habitualmente la programación web es de un sólo «hilo», por lo que poner más procesadores o con más núcleos no acelera una ejecución. La única solución es aumentar los MHz, pero doblar los MHz tampoco implica que se reduzca el tiempo a la mitad (ni mucho menos) por lo que el coste para lograrlo suele ser muy elevado.

Pero este programador se olvidó de algo importante que aprendió en sus estudios y que es fundamental es informática, sistemas operativos y multiprogramación en general: la intercalación.

Si no la hubiese tenido en cuenta se habría dado cuenta que lo que tocaba era «adelantar» la generación del HTML para hacerlo progresivo y empezar la compresión y el envío mientras se hacen las demás consultas a la base de datos. Así el dibujo original, con los «servidores más lentos» le hubiese quedado algo como lo siguiente:

EEBBBBBBBBBBHHHHH
  AAAEEEEEEEEEEEEEEEEEE

En se empieza a enviar inmediatamente el contenido que es inmediato o necesita muy pocas consultas  (como la cabecera y la barra lateral del Méneame) para que el navegador pueda empezar a interpretarlo. Con estos números el tiempo total es de 23 tics (40% de mejora) y 5 tics para la visualización (75% de mejora). Los resultados son mucho más espectaculares sin cambiar nada de equipamiento, sólo «adelantando» la evaluación de los templates (y/o deshabilitando el output buffer).

Por supuesto esta «espectacular» mejora depende mucho de la aplicación. Menéame es un caso más o menos extremo en sus tiempos, la lógica y consultas a la base de datos están muy optimizadas y por lo tanto consumen muy poco tiempo relativo. Los siguientes son tiempos resultantes de 100 mediciones sobre los servidores en producción a una hora de mucho tráfico (hoy viernes entre las 12:00 a 12:30 hs)

  1. La lógica más base de datos (B): media 0.03348 seg (desviación estándar 0.02155)
  2. Lógica más base de datos (B) + generación HTML (H): 0.04663 seg (desviación estándar  0.02903)
  3. De #1 y #2 se obtiene que la media de  H es: 0.01315 seg

Como daría mucho trabajo rehacer los programas y plantillas hice la prueba habilitando y deshabilitando el buffering y la compresión. La siguiente tabla muestra el tiempo total medio (en segundos, contando el establecimiento de una nueva conexión TCP/IP) que tarda en bajar el HTML de la portada (14 KB comprimidos, 57 KB sin comprimir, aproximadamente):

Sin compresión Con compresión
Sin buffer Con buffer Sin buffer Con buffer
ONO 12 Mbps 0.52 0.61 0.32 0.34
3G Vodafone 1.09 1.12 0.86 0.89

Se puede observar que sin buffering es algo más rápido pero la diferencia es mínima. Sí se nota la diferencia con la compresión habilitada, se llega hasta el 38% de reducción del tiempo.

Conclusiones

No uses el buffering de salida si no estás seguro de lo que haces y/o has hecho mediciones de tiempo. En general irá mejor sin buffer, y consumirá menos recursos del servidor (fundamentalmente memoria RAM).

Si tu aplicación con plantillas es compleja y el tiempo de procesamiento y consultas a la base de datos es relativamente grande, serializa, no generes todo a partir de un sólo template que llamas al final. Además ahorrarás memoria RAM y ciclos de CPU. En caso que no puedas serializar la generación de cada «objeto», al menos genera una plantilla independiente con la cabecera HTML y envíala lo antes posible, así el navegador podrá bajar en paralelo los CSS y javascripts.

Nota final sobre los dispositivos móviles e iPads

Ten en cuenta que muchos usuarios usan sus móviles o iPads para navegar, haz que sean más felices con sus cacharros cuando naveguen por tu sitio, que para eso se gastaron un pastón. Hay algunos trucos muy fáciles, como no mostrar contenido secundario, no cargar APIs «pesados», y usar el selector @media del CSS para ajustar tamaños, márgenes o no mostrar secciones completas.

Menéame tiene su sitio optimizado para móviles desde hace tiempo. Pocos lo conocían, así que hemos implementado la redirección automática al sitio de móviles si se detecta que el cliente es de móviles (teníamos más de 10.000 visitas diarias). Mucha gente se quejaba de esta redirección, así que la restringimos a que sólo se haga si se accede directamente a una noticia desde un sitio externo, o si se usa el enlace corto tipo http://m.menea.me/m5za (lo usamos por ejemplo en los envíos automáticos a Twitter).

Para aligerar la navegación por el sitio principal a esos usuarios hemos hecho varias cosas:

  • No se genera el contenido de la barra lateral derecha (ni la publicidad superior).
  • Se cambia el tamaño de la fuente, algunos márgenes y marca en el CSS como «oculta» (display: none) aprovechando el selector @media de los CSS:
/* Definitions for mobile, iPad and TVs */
@media print,tv,handheld,all and (max-device-width: 780px) {
body {
font-size: medium;
}

.sneaker {
font-size: small;
}

#wrap {
min-width: 320px;
}

#sidebar {
display:none;
}

#singlewrap, #newswrap {
margin: 5px 5px 0px 5px;
}

#footwrap {
margin: 5em 5px 0 5px;
clear: both;
}

.banner-top {
width: 470px;
}
}
  • No se muestran los mapas ni se carga el API de Google Maps.
  • No se muestra el megabanner sino un anuncio de AdSense más pequeño.