martes, 27 de octubre de 2009

Tuneando la performance de sitios web

Este post describe distintas técnicas de optimización de performance para sitios web. Se basa en la experiencia de una aplicación de reporting construida en asp.net con fuertes constraints de performance (tiempo de ejecución por debajo de 2 segundos para accesos locales y 8 segundos para accesos remotos), teniendo una conectividad pobre desde sitios remotos.
Sin embargo estas técnicas aplican a cualquier aplicación tipo web/html. Las implementaciones y ejemplos de código se corresponden a una aplicación asp .net 2.0, c#, IIS 6, pero pueden extenderse a otras tecnologías.
Un buen lugar para empezar a abordar este tema es el sitio de best practices de yahoo (se pueden encontrar muy buenas recomendaciones):

Según una investigación realizada por ellos (http://yuiblog.com/blog/2006/11/28/performance-research-part-1/), al acceder a una aplicación web el tiempo de download se distribuye un 20% en html y el 80% restante en otro tipo de contenido (javascript, imágenes, etc.) Por lo tanto la primera recomendación es:

Minimizar la cantidad de gets (roundtrips)

Esto puede conseguirse de distintas maneras:

Unificar archivos javascript

En nuestro caso tenemos un archivo .js para cada funcionalidad, de manera tal de hacer el desarrollo más modular. Al armar la version release los unificamos y comprimimos en un único archivo. Lo mismo se puede hacer para las hojas de estilo. Se debe tener en cuenta que cuanto mejor formado esté el código javascript (usando llaves para todas las instrucciones aunque no sea necesario, ";" al final de cada línea, etc.) mayor será el nivel de compresión que se puede aplicar.

Para unificar los archivos utilizamos una simple pagina .aspx que contiene un include a cada fuente javascript. Luego la aplicación detecta si está en modo debug o relase (usando HttpContext.Current.IsDebuggingEnabled) y en función de esto genera del lado del server las referencias a fuentes javascript necesarias.

Juntar varias imágenes en una (css sprite)

Esta ténica se conoce como css sprite. Básicamente se arma una imagen con las imágenes más utilizadas por la aplicación y se referencia con estilos a porciones de esta imagen. Ejemplo:
Definicion de clase para icono de ayuda:
.imgHelpIcon { background: url('/App/Cached/sprite.gif') 0 0 no-repeat; font:1pt; background-position: 10 -20px; width: 15px; height: 15px; }

Uso del estilo:
<span class="imgHelpIcon"></span>

Utilizar cache infinito para recursos estáticos

Esta técnica tiene gran impacto en el download, sobre todo si la mayoría de los usuarios utilizan frecuentemente la aplicación. Con esto se evitan los gets a estos recursos, incluso si el cache del browser está configurado como "every visit to the page".

En nuestro caso lo implementamos de la siguiente manera. Configuramos en el webserver una carpeta cached con fecha de exipiracion en 2035. De esta manera disponemos de 2 altenativas para almacenar los recursos estáticos (con y sin cache). Por ejemplo en modo debug la aplicación utiliza los scripts de la carpeta app/scripts. En cambio, en modo release utiliza app/cached/scripts.

Para los recursos que pueden cambiar (como js y css) los versionamos agregando un parametro a la url. Por ejemplo:
<script scr="cached/all_js.js?v=1.01"></script>

Para recuperar este valor usamos el método ConfigurationManager.AppSettings.Get("jsVersion"). Cada vez que se arma un release, incrementamos este número solo si se realiza un cambio sobre algún fuente javascript.

Aplicando esta técnica a los recursos estáticos, los gets de usuarios frecuentes se redujeron a práctiamente solo uno para obtener el html (ya que el browser recupera todos los recursos estáticos del cache). Esta técnica mejora notablemente los tiempos de acceso desde clientes remotos.
Quitar seguridad integrada a recursos estáticos

Los recursos con seguridad integrada implican 2 o 3 roundtrips al server según el tipo de autenticación. Por lo tanto es conveniente evitarla siempre
que sea posible.

Quitar seguridad integrada a servicios ajax

En algunos servicios ajax se necesita información del usuario para resolver ciertas funcionalidades. Para evitar la seguridad integrada, el usuario forma parte del estado en el server (más adelante se explica mejor cómo resolver esta cuestión). De esta manera todos los servicios ajax se ejecutan sin seguridad integrada.

Quitar seguridad integrada de aplicación.

Aplicar esta técnica nos llevó un poco más de trabajo. Básicamente solo una página (authenticate.aspx) está configurada con seguridad integrada. Cuando el usuario ingresa a la aplicación, esta intenta obtener el usuario del estado. Sino lo puede obtener, entonces asume que no está autenticado, lo redirecciona a authenticate.aspx para que obtenga las credenciales del usuario, lo guarde en el estado y lo redireccione nuevamente a la aplicación. Luego el usuario navega por la aplicación sin seguridad integrada (la aplicación verifica que el usuario esté autenticado usando una variable de sesión).

Al implementar este mecanismo nos encontramos con un problema grave. IE 6 tiene un bug donde a veces no envía los parámetros enviados por post (http://blogs.msdn.com/david.wang/archive/2005/12/01/HTTP-POST-Fails-for-Anonymous-Authentication.aspx). Esto lo solucionamos configurando acceso anónimo y seguridad integrada a la vez.

Si reducimos todo lo posible la cantidad de pedidos al server (GETs) lo siguiente que podemos hacer es reducir el tamaño de la información intercambiada.
Reducir el tamaño de los gets / responses

Comprimir javascript y css

Existen varias herramientas para hacer esto (http://developer.yahoo.com/yui/compressor/). Estas herramientas básicamente eliminan espacios, comentarios, incluso algunas hasta ofuscan el código para reducir su tamaño.

Reducir tamaño html

La idea es simple, cuanto menos se envíe, más rápido va a terminar la transferencia. Se pueden hacer varias cosas para reducir el html:
  • Eliminar espacios
  • Utilizar nombres abreviados para los nombres de estilos y objetos
  • Maximizar el uso de hojas de estilo, dejando el html mucho más limpio. Una herramienta muy útil para hacer esto son los css selector (http://www.w3schools.com/Css/css_syntax.asp)
  • Manejo de estado
La aplicación persiste el estado en el server. En lugar de utilizar campos hidden en el cliente (que se envían en cada postback), utiliza variables de sessión para persistir los cambios realizados sobre un reporte (filtro, paginado, drill, etc.). De esta manera el cliente solo envía al server lo que cambia, reduciendo considerablemente el tamaño de los requests.

Mantener estado en el cliente usando user behaviour

Esto se puede aplicar en los casos que solo se necesita persistir información en el cliente y esta no es requerida por el server (ejemplo: estado de apertura de un árbol). Internet explorer ofrece un behavour que permite persistir información en el cliente (incluso cuando el usuario cierra el browser):

Ajax + json / xml

El los casos donde los servicios ajax devuelven datos, usamos json ya que es más liviano que xml. Solo en algunas ocaciones, cuando el servicio ajax devuelve html, usamos xml. En este caso el html se devuelve dentro de un cdata para evitar problemas de encodeo.


Otrás técnicas que no usamos, pero también son de ayuda

Compresión gzip

Es posible configurar IIS para que comprima gzip archivos javascript y estilos. El beneficio de la compresión gzip es notorio cuando se accede remotamente. También es posible comprimir recursos dinámicos como páginas aspx.

Usar varios dominios para el download de contenido

Por default el browser usa 2 conexiones por dominio para hacer el download. Si el contenido cambia (como ocurre por ejemplo en los portales), es posible utilizar distintos dominios para el mismo, de manera tal de aumentar la cantidad de conexiones usadas por el browser.

En el sitio de yahoo hay otra recomendaciones:

Herramientas

En nuestro caso fue fudamental usar máquinas virtuales como entorno de desarrollo para poder experimentar con estas técnicas y contar con máquinas tipo desktop estandar en sitios remotos para poder evaluar el impacto de las mismas. También nos sirvió de mucho usar un debugger http como fiddler (http://www.fiddlertool.com/fiddler)

Fiddler permite ver gráficamente cómo es el tiempo de download. Para ello, se debe activar el modo streaming (http://www.fiddlertool.com/Fiddler/help/Streaming.asp). Con esta herramienta es fácil encontrar puntos de mejora, revisar errores http 404, ver el impacto de aplicar estas técnicas, etc.

No hay comentarios.: