Los arreglos podrían estar destruyendo el rendimiento de tu applicación

July 19, 2019

...

Un tema que siempre es interesante conversar es el rendimiento de las aplicaciones. Puedo estar equivocado pero creo que a la mayoría de los desarrolladores en algún momento de sus vidas pensaron en cómo poder mejorar el rendimiento de una aplicación ya que esta funciona lento, se demora en procesar data o cualquier otro problema a fin.

Es muy importante mencionar de que el rendimiento en una aplicación no es necesariamente importante cuando empiezas a crear un proyecto, siempre debe ser más importante que la aplicación funcione y que otros desarrolladores puedan entender lo que se esta construyendo. Escribir código que sea de alto performance no es siempre una tarea sencilla, hay muchos aspectos a considerar sobre todo de arquitectura. Sin embargo existen unos pequeños consejos que pueden ayudarte a escribir código que tenga buen desempeño desde un comienzo sin que tengas que pasar por los dolores de cabeza donde los usuarios se quejan porque la aplicación funciona lento. En este post vamos a ver algunos de los más comunes y cómo podemos evitar dolores de cabeza futuros con solo unos pequeños cambios en nuestro código.

Los arreglos son lentos

Quizás esta afirmación sea un poco agresiva y algunos no estén de acuerdo. Los arreglos son un tipo de dato que viene por defecto en el runtime de javascript y todos los tutoriales nos enseñan de que debemos utilizarlos para todo de lista que podamos tener. Sin embargo no nos explican que cuando debemos realizar cruces de datos entre distintos arreglos y estos ya empiezan a tener magnitudes del orden de los miles o cientos de miles, utilizar arreglos no es la mejor opción al tener que procesar la data en el cliente, ya sea aplicación móvil o aplicación de escritorio.

Supongamos que debemos generar un reporte donde debemos exportar una planilla de datos. Esta planilla después será utilizada para generar reportes personalizados por el mismo cliente así que debemos preocuparnos de sacar data cruzada.

En este requerimiento tenemos un listado de usuarios y queremos saber los videojuegos que estos han comprado.

const users = [
  { id: '1', name: 'Chanchito feliz' },
  { id: '2', name: 'Juan' },
]

const products = [
  { id: '1', name: 'Call of duty' },
  { id: '2', name: 'Pacman' }
]

const purchases = [
  { id: '1', user_id: '1', product_id: '1' },
  { id: '2', user_id: '1', product_id: '2' },
  { id: '3', user_id: '2', product_id: '1' },
]

En este ejemplo mantendremos la cantidad de datos a procesar bajos para que sea fácil de entender el ejemplo y explicar qué ocurre en cada paso.

La planilla que nos piden generar debe contener lo siguiente:

Usuario Producto
Chanchito feliz Call of duty
Chanchito feliz Pacman
Juan Call of duty

En este caso Tenemos tenemos múltiples alternativas para solucionar este problema, sin embargo todas necesitan que iteremos nuestros arreglos, lo mas sensato seria utilizar las compras y empezar a crear un nuevo objeto en base a los ids e ir a buscarlos con find:

const data = purchases.map(x => ({
  user: users.find(u => u.id === x.user_id),
  product: products.find(p => p.id === x.product_id),
}))

Sin embargo esto tiene una enorme desventaja. El método find de los arreglos comenzará a iterar cada uno de los elementos dentro de un arreglo hasta encontrar el que le interesa y luego devolverlo, si el elemento en cuestión se encuentra al final del arreglo, el método de find iterará el arreglo completo antes de devolver el elemento, y si no lo encuentra, de igual manera lo iterará completo, con la pequeña cantidad de elementos que nosotros tenemos en este listado esto no seria un problema, y si no es un problema, no deberías cambiar este código. Sin embargo, si existe un cliente que tenga miles de usuarios, productos y que ademas existan muchas compras dentro de nuestra aplicación, este es un problema donde podemos bloquear la interfaz o servidor. Algunos podrían pensar que cosas como estas no deberían ser preocupación y que podrían ser perfectamente enviados a un proceso de cola para que este genere el reporte... lo cual esta en lo cierto. Sin embargo debemos realizar modificaciones de arquitectura y también más código para poder resolver este problema.

En total, si tenemos 10000 compras, 10000 usuarios y 1000 productos, la cantidad de operaciones que se realizaran puede llegar a ser del orden de los 100 millones.

Afortunadamente existe una alternativa donde nosotros podremos cambiar nuestro código para que la cantidad de iteraciones sea de un máximo de 21 mil en el peor escenario utilizando solo objetos, sin introducir una librería externa, aunque también podríamos utilizar Map.

const indexedUsers = users.reduce((acc, el) => ({
  ...acc,
  [el.id]: el,
}), {})

Momento! Demos una pausa acá ya que hay algunas cosas que analizar. En este momento estamos haciendo uso de una de mis funciones favoritas en javascript, la cual es reduce. Reduce recibe 2 argumentos: una función y un valor inicial, que en este caso es un objeto literal. A la función que recibe reduce se le inyecta el valor inicial, que en este caso es nuestro objeto vacío y se le asigna al primer argumento acc, el segundo argumento el es un elemento del arreglo que esta siendo iterado, en este caso el primer usuario, esta función devolverá un nuevo objeto el cual esta copiando todas las propiedades de nuestro acumulador acc, que por ahora es vacío, y le agrega una propiedad de manera dinámica, que en este caso es el id del usuario, finalmente le agrega el usuario a la propiedad de nombre del id del usuario. Esta función se ejecutará una segunda vez, donde ahora acc será el objeto que nosotros retornamos en la ejecución, pasada pero ahora agregaremos al siguiente usuario a este objeto.

const indexedUsers = users.reduce((acc, el) => ({
  ...acc,
  [el.id]: el,
}), {})

// indexedUsers === {
//   1: { id: 1, name: 'Chanchito feliz' },
//   2: { id: 2, name: 'Juan' },
// }

La gracia que tiene ordenar los arreglos como objetos es que podremos obtener la propiedad del objeto de manera rápida, esta instrucción no itera los objetos en javascript por lo que es mucho mas rápido preguntar por la propiedad de un objeto que buscar un elemento dentro de un arreglo. Realizamos la misma operación con nuestros productos:

const indexedProducts = products.reduce((acc, el) => ({
  ...acc,
  [el.id]: el,
}), {})

Por lo que nuestro código anterior queda así:

const indexedUsers = users.reduce((acc, el) => ({
  ...acc,
  [el.id]: el,
}), {})

const indexedProducts = products.reduce((acc, el) => ({
  ...acc,
  [el.id]: el,
}), {})

const data = purchases.map(x => ({
  user: indexedUsers[x.user_id],
  product: indexedProducts[x.product_id],
}))

Listo! Esto hace que nuestro código sea mucho más eficiente, claro esta que debemos recorrer todos los elementos para poder indexarlos por su ID, por lo que debemos evaluar si estos deberían ser indexados al momento de iniciar nuestra app, pero por lo general este es un acercamiento que puede solucionar un montón de dolores de cabeza.

Sin embargo la duplicidad de código me está matando así que crearemos una función auxiliar para las funciones de reduce:

const indexBy = (key, xs) => xs.reduce(acc, el) => ({
  ...acc,
  [el[key]]: el,
})

const indexedUsers = indexBy('id', users)
const indexedProducts = indexBy('id', products)

const data = purchases.map(x => ({
  user: indexedUsers[x.user_id],
  product: indexedProducts[x.product_id],
}))

Ahora sacamos la función de indexBy a una función auxiliar por lo que no es necesario que nos repitamos constantemente. Esta función puedes verla en Lodash como keyBy o en Ramda como indexBy.


Suscríbete

Suscríbete a la lista para más cursos, posts y videos tutoriales. Prometo no enviarte más de un correo semanal 🙏

Creado por Nicolás Schürmann ingeniero e instructor de software. Cuando no está programando, esta frente a una cámara dictando cursos, creyéndose youtuber o apoyando a sus alumnos. Puedes seguirlo en twitter o también suscribirte a su canal de youtube. Considera comprar sus cursos por este medio y así apoyas al instructor.