Optimización Javascript

Es evidente que cuando desarrollamos, intentamos hacer código compacto, veloz, sencillo y mantenible, pero la premura de tiempo con las fechas de entrega siempre hace que al final, hagamos las cosas de una manera que no es la óptima en alguno de estos sentidos.

Tradicionalmente en el mundo empresarial se deja de lado la eficiencia del código, es decir su velocidad de ejecución y su tamaño, o sea la cantidad de recursos necesarios para su ejecución. Salvo contadas excepciones, versiones nuevas de aplicaciones necesitan más tiempo de ejecución para realizar la misma tarea que las versiones iniciales de la misma aplicación.
Al final este efecto acaba compensándose (al menos de cara a los usuarios) por la Ley de Moore en el hardware. La nueva versión es más lenta, pero los ordenadores de los usuarios son más rápidos.

El hecho de dejar de lado el rendimiento tanto de espacio como de tiempo, se debe fundamentalmente a que los otros dos aspectos importantes (simplicidad y mantenibilidad) casi nunca se pueden dejar de lado.

La simplicidad en el desarrollo si el plazo de entrega es corto, siempre se obtiene. Los timmings apretados obligan a buscar esquemas de desarrollo simples para poder cumolir con las diferentes fases de entrega. Hoy en día la mayoría de nosotros nos encontramos con plazos que son cuanto menos breves. En pocas palabras, o lo haces simple, o el proyecto no sale.

La mantenibilidad es algo que se asume como prerequisito en cualquier proyecto informático. Es importante para la empresa que el código pueda ser retomado por cualquier persona/personas de dentro o fuera del equipo de desarrollo con el mínimo periodo de entrenamiento posible.
Además se requiere que el tiempo de corrección de errores e incidencias se reduzca a la mínima expresión.
Si a todo esto sumamos que necesitamos tener una base de código lo suficientemente mantenible como para liberar nuevas versiones más rápido que nuestros competidores, veremos claramente que este es un punto que no podemos dejar al margen: un proyecto tiene que ser mantenible.

Personalmente hacía bastante tiempo que no me encontraba en mi trabajo con la necesidad de optimizar a conciencia alguna parte de uno de mis aplicativos.
Supongo que a la mayoría de los que no desarrollamos aplicaciones de bajo nivel es algo que nos ocurre con frecuencia.

Aunque siempre hay excepciones a esto. Yo mismo por ejemplo, hace un par de semanas, me encontré con la necesidad de tener que optimizar una pequeña parte de la aplicación que estamos desarrollando.

La aplicación en cuestión es una extranet desarrollada en HTML, PHP y MySQL. Aunque pueda parecer que el punto crítico de rendimiento estaba en la BD como suele ser habitual, en este caso concreto no fue así.

La sección de administración de contenidos, en uno de sus subapartados, manejaba varias listas de tipo multiselect, que se generaban dinámicamente con PHP y MySQL. El usuario podía manipular estos datos en su navegador cliente con Javascript, y al final del proceso se volvían a mandar los datos al PHP, que los actualizaba en la BD.

El problema estaba en que la ejecución del código Javascript era extremadamente lenta. Procesos sencillos tardaban unos pocos segundos, pero cosas más complicadas, tardaban 1 o 2 minutos. En este caso la Ley de Moore no ayudaba mucho, ya que esto se ejecutaba en mi propio PC del trabajo. Un Pentium 4 a 2,8 Ghz. Estaba claro que si en mi ordenador esto ya era un problema, en los ordenadores tipo de los usuarios, iba a ser mucho peor.

No quedaba más remedio que optimizar el código Javascript.

Hay ciertas reglas de optimización que son generales independientemente del lenguaje: Desenrrollado de bucles, índices inversos, expresiones precalculadas, …

Así que empecé a optimizar siguiendo las reglas que he mencionado, reglas que por otra parte descubrí en mis tiempos de C y ensamblador.

Sin duda después de esto el rendimiento global aumentó, en aproximadamente un 500%, lo cual reducía el tiempo de ejecución de 2 minutos a 30 segundos. Seguía siendo demasiado.

Empíricamente después de haber realizado algunas pruebas, descubrí que el cuello de botella se encontraba en el acceso a propiedades, métodos y eventos de objetos. Especialmente de aquellos que tienen representación visual (DOM), que eran el grosso de la aplicación. Empecé a precalcular los valores a los que necesitaba acceder, y algunos retoques más.

Cuatro horas después de haber comenzado, ya lo tenía todo listo. Había pasado de 2 minutos a 3 segundos. Lo que significaba que la nueva versión era 50 veces más rápida que la original. O lo que es lo mismo un 5000%.

A continuación pongo el código de los métodos que modifiqué, su versión original y la optimizada.

Método SelectAdd() original
function SelectAdd (pfrmForm)
{
if (pfrmForm.elements['role_id'].selectedIndex>0)
{
for (iCount=0; iCount<pfrmForm.elements['user_id[]'].options.length; iCount++)
{
for (iCount2=0 ;iCount2<pfrmForm.elements['event_id[]'].options.length; iCount2++)
{
for (iCount3=0; iCount3<pfrmForm.elements['userevent_user[]'].options.length; iCount3++)
{
if ((pfrmForm.elements['user_id[]'].options[iCount].selected) && (pfrmForm.elements['event_id[]'].options[iCount2].selected) &&
(pfrmForm.elements['user_id[]'].options[iCount].value==pfrmForm.elements['userevent_user[]'].options[iCount3].value) &&
(pfrmForm.elements['event_id[]'].options[iCount2].value==pfrmForm.elements['userevent_event[]'].options[iCount3].value))
{
alert('L'usuari ' + pfrmForm.elements['userevent_user[]'].options[iCount3].text + ' ja està afegit a l'event ' + pfrmForm.elements['userevent_event[]'].options[iCount3].text);
return;
}
}
}
}

for (iCount=0; iCount<pfrmForm.elements['user_id[]'].options.length; iCount++)
{
for (iCount2=0; iCount2<pfrmForm.elements['event_id[]'].options.length; iCount2++)
{
if ((pfrmForm.elements['user_id[]'].options[iCount].selected) && (pfrmForm.elements['event_id[]'].options[iCount2].selected))
{
oOption=document.createElement('OPTION');
pfrmForm.elements['userevent_user[]'].options.add(oOption);
oOption.innerText=pfrmForm.elements['user_id[]'].options[iCount].text;
oOption.value=pfrmForm.elements['user_id[]'].options[iCount].value;
oOption.selected=true;

oOption=document.createElement('OPTION');
pfrmForm.elements['userevent_role[]'].options.add(oOption);
oOption.innerText=pfrmForm.elements['role_id'].options[pfrmForm.elements['role_id'].selectedIndex].text;
oOption.value=pfrmForm.elements['role_id'].options[pfrmForm.elements['role_id'].selectedIndex].value;
oOption.selected=true;

oOption=document.createElement('OPTION');
pfrmForm.elements['userevent_event[]'].options.add(oOption);
oOption.innerText=pfrmForm.elements['event_id[]'].options[iCount2].text;
oOption.value=pfrmForm.elements['event_id[]'].options[iCount2].value;
oOption.selected=true;
}
}
}
}
else
{
alert('Has de seleccionar un role.');
pfrmForm.elements['role_id'].focus();
}
}

Método SelectAdd() optimizado
function SelectAdd (pfrmForm)
{
role_id=pfrmForm.elements['role_id'];
if (role_id.selectedIndex>0)
{
sResult='';
user_id=pfrmForm.elements['user_id[]'];
event_id=pfrmForm.elements['event_id[]'];
event_id_length=event_id.options.length-1;
userevent_user=pfrmForm.elements['userevent_user[]'];
userevent_user_length=userevent_user.options.length-1;
userevent_role=pfrmForm.elements['userevent_role[]'];
userevent_event=pfrmForm.elements['userevent_event[]'];
userevent_user_options=userevent_user.options;
userevent_role_options=userevent_role.options;
userevent_event_options=userevent_event.options;
event_id_options=event_id.options;
user_id_options=user_id.options;

for (iCount=user_id_options.length-1; iCount>=0; iCount–)
{
user_id_selected=user_id_options[iCount].selected;
user_id_value=user_id_options[iCount].value;
for (iCount2=event_id_length; iCount2>=0; iCount2–)
{
event_id_selected=event_id_options[iCount2].selected;
event_id_value=event_id_options[iCount2].value;
for (iCount3=userevent_user_length; iCount3>=0; iCount3–)
{
if ((user_id_selected) && (event_id_selected) &&
(user_id_value==userevent_user_options[iCount3].value) &&
(event_id_value==userevent_event_options[iCount3].value))
{
sResult=sResult + 'L'usuari ' + userevent_user_options[iCount3].text + ' ja està afegit a l'event ' + userevent_event_options[iCount3].text + '.
';
}
}
}
}
if (sResult.length>0)
{
alert(sResult);
return;
}
role_id_text=role_id.options[role_id.selectedIndex].text;
role_id_value=role_id.options[role_id.selectedIndex].value;
event_id_length=event_id_options.length-1;
for (iCount=user_id_options.length-1; iCount>=0; iCount–)
{
user_id_selected=user_id_options[iCount].selected;
user_id_value=user_id_options[iCount].value;
user_id_text=user_id_options[iCount].text;
for (iCount2=event_id_length; iCount2>=0; iCount2–)
{
if ((user_id_selected) && (event_id_options[iCount2].selected))
{
oOption=document.createElement('OPTION');
userevent_user_options.add(oOption);
with (oOption)
{
innerText=user_id_text;
value=user_id_value;
selected=true;
}
oOption=document.createElement('OPTION');
userevent_role_options.add(oOption);
with (oOption)
{
innerText=role_id_text;
value=role_id_value;
selected=true;
}
oOption=document.createElement('OPTION');
userevent_event_options.add(oOption);
with (oOption)
{
innerText=event_id_options[iCount2].text;
value=event_id_options[iCount2].value;
selected=true;
}
}
}
}
}
else
{
alert('Has de seleccionar un role.');
role_id.focus();
}
}

Método SelectRemove() original
function SelectRemove (pfrmForm)
{
for (iCount=0; iCount<pfrmForm.elements['userevent_user[]'].options.length; iCount++)
{
if (pfrmForm.elements['userevent_user[]'].options[iCount].selected)
{
pfrmForm.elements['userevent_role[]'].remove(iCount);
pfrmForm.elements['userevent_user[]'].remove(iCount);
pfrmForm.elements['userevent_event[]'].remove(iCount);
}
}
}

Método SelectRemove() optimizado
function SelectRemove (pfrmForm)
{
userevent_user=pfrmForm.elements['userevent_user[]'];
userevent_role=pfrmForm.elements['userevent_role[]'];
userevent_event=pfrmForm.elements['userevent_event[]'];
userevent_user_options=userevent_user.options;
for (iCount=userevent_user_options.length-1; iCount>=0; iCount–)
{
if (userevent_user_options[iCount].selected)
{
userevent_role.remove(iCount);
userevent_user.remove(iCount);
userevent_event.remove(iCount);
}
}
}

Método SelectSubmit() original
function SelectSubmit (pfrmForm)
{
for (iCount=0; iCount<pfrmForm.elements['userevent_user[]'].options.length; iCount++)
{
pfrmForm.elements['userevent_user[]'].options[iCount].selected=true;
pfrmForm.elements['userevent_role[]'].options[iCount].selected=true;
pfrmForm.elements['userevent_event[]'].options[iCount].selected=true;
}
pfrmForm.action='admin_userevent_save.php';
pfrmForm.submit();
}

Método SelectSubmit() optimizado
function SelectSubmit (pfrmForm)
{
userevent_user=pfrmForm.elements['userevent_user[]'];
userevent_role=pfrmForm.elements['userevent_role[]'];
userevent_event=pfrmForm.elements['userevent_event[]'];

userevent_user_options=userevent_user.options;
userevent_role_options=userevent_role.options;
userevent_event_options=userevent_event.options;
for (iCount=userevent_user_options.length-1; iCount>=0; iCount–)
{
userevent_user_options[iCount].selected=true;
userevent_role_options[iCount].selected=true;
userevent_event_options[iCount].selected=true;
}
pfrmForm.action='admin_userevent_save.php';
pfrmForm.submit();
}


Optimización Javascript

1 comentario en “Optimización Javascript”

Deja un comentario