Cómo implementar la paginación "almacenable en caché" de contenido que cambia con frecuencia

Siempre que necesitemos mostrar una gran cantidad de contenido almacenado en el backend, lo dividimos en trozos y luego los cargamos de a uno por vez. Este es un enfoque común, y hay varias razones por las que es tan bueno:

  1. Mejora la experiencia del usuario. Cargar una pequeña página individual lleva menos tiempo, por lo que el usuario puede comenzar a consumir contenido más rápido.
  2. Reduce la carga en la red. Una sola página es pequeña y ligera en términos de ancho de banda. Además, podemos optimizar el consumo de batería y red para dispositivos móviles ajustando el tamaño de una sola página.
  3. Reduce la carga en la parte trasera. Procesar piezas más pequeñas es más rápido que las más grandes. Los usuarios generalmente no necesitan todo el contenido a la vez, por lo que la carga promedio por usuario es menor.

Todos ganan. En la mayoría de los casos. Pero no todos ellos. Como suele suceder con las soluciones genéricas, cuanto más específico de dominio se vuelve, mejor es la solución que se puede encontrar. En este artículo, quiero compartir una solución interesante para uno de esos casos.

Definiendo la tarea

Imaginemos una colección que cambia con frecuencia con el tiempo. Por ejemplo, podríamos mirar una lista de artículos que un usuario aplaudió en Medium, o una lista de deseos en una aplicación de compras, o un historial de las acciones de cualquier otro usuario en general. Los usuarios pueden "agregar" tantos elementos a esa colección como quieran.

Nuestra tarea es mostrar esta colección de manera conveniente, mientras hacemos todo lo posible para evitar el abuso de la red y, por lo tanto, el abuso de la batería y el ancho de banda.

Problemas con la paginación.

Una de las formas de minimizar el uso de la red es almacenar en caché las respuestas. La mayoría de las aplicaciones móviles y web dependen en gran medida del caché HTTP. Guarda las respuestas durante un período de tiempo especificado en el encabezado de la respuesta. Cada vez que una aplicación realiza una solicitud, el cliente HTTP intenta obtener una respuesta correspondiente del caché. Solo si no está disponible realiza una llamada real al backend.

A veces, este tipo de almacenamiento en caché no funciona bien para contenido paginado. Si la colección se cambia con frecuencia y el contenido debe estar actualizado, entonces simplemente no tiene sentido almacenar la respuesta en caché. Como ejemplo, imaginemos el siguiente escenario:

  1. El usuario abre la lista de artículos que aplaudieron aquí, en Medium. La primera página se obtiene del backend.
  2. Después de eso, el usuario buscó algo nuevo, encontró otro artículo interesante y decidió recomendarlo.
  3. Ahora quieren revisar la lista de artículos que recomendaron nuevamente.

La aplicación necesita realizar la misma solicitud para la primera página, pero el resultado es diferente. La respuesta no se puede almacenar en caché.

Si su tarea específica de dominio le permite reorganizar elementos en esta colección, entonces su problema es aún peor. Por la misma razón: la respuesta está cambiando constantemente.

Otro enfoque

Echemos un vistazo más de cerca a la primera página de datos obtenida del back-end. La respuesta en sí es una lista de elementos en un orden particular. Es improbable que cada elemento cambie. Lo que cambia es el orden de los elementos y los elementos que están en esta lista.

Esto significa que si podemos obtener por separado el orden de las ID de los artículos y los detalles del artículo, la segunda llamada puede almacenarse en la memoria caché. De hecho, incluso el almacenamiento en caché de la segunda llamada no es sencillo, pero lo haremos. Por ahora, hagamos una solicitud por separado para cada artículo:

Como puede ver en el diagrama anterior, debido a que es poco probable que los elementos cambien, podemos almacenar en caché las llamadas detalladas de los elementos. Desafortunadamente, tal división multiplicará la cantidad de llamadas de red por un orden de magnitud. Pero hay algo que podemos hacer al respecto.

¿Qué es lo que realmente queremos?

Si solo solicitamos un montón de artículos, encontraremos el mismo problema que el enfoque genérico de paginación. El caché HTTP no funcionará como queremos, así que escribamos el nuestro usando una lógica similar pero más deliberada.

Este caché no va a almacenar lotes, sino elementos individuales durante un período de tiempo determinado. Tomaremos la respuesta, accederemos a sus encabezados HTTP y recuperaremos información sobre el tiempo de almacenamiento en caché. Luego, colocaremos cada elemento individualmente en el caché. La próxima vez que necesitemos mostrar elementos, podemos acceder fácilmente a los almacenados en caché y solicitar el resto. En código parece más fácil de lo que parece:

Veamos rápidamente el código. El método getOrderedItemsIds () devuelve el orden de los elementos y está paginado. La parte más importante es el método getItemsByIds (). Este es un lugar donde primero verificamos qué elementos están en el caché y luego solicitamos el resto del back-end. Tenga en cuenta que, en aras de la simplicidad, el código anterior es sincrónico y probablemente no se compilará.

Después de implementar este enfoque, la adición de un nuevo elemento al encabezado de la lista provocará una solicitud para el pedido de uno de los ID de los elementos y los detalles del nuevo elemento. El resto proviene del caché.

Se producirá un par similar de llamadas para cada página consecutiva. La idea principal es que podemos recuperar la mayoría de los detalles del elemento del caché. Pero desafortunadamente tenemos que solicitar el pedido de ID de artículos para cada página.

Hacerlo mejor

Las ID de los elementos suelen ser un objeto pequeño como una cadena o un identificador único universal (UUID). Por lo tanto, podemos enviar páginas más grandes. Aumentar la cantidad de ID de artículos devueltos por una llamada de pedido disminuye la cantidad de llamadas, sin abusar del ancho de banda de la red.

Por ejemplo, en lugar de solicitar 20–40 ID de artículos, podemos solicitar 100-200. Más tarde, la capa de IU puede moderar la cantidad de detalles de elementos que deben mostrarse y solicitarlos en consecuencia. Luego, una secuencia de llamadas se verá así:

  1. Solicite los primeros 100 ID de elementos y guárdelos en la memoria.
  2. Solicite detalles de los primeros 20 elementos (por supuesto, almacénelos en caché) y muéstrelos al usuario.
  3. Después de que el usuario se desplace por los primeros 20 artículos, solicite el segundo lote de 20 detalles de artículos.
  4. Repita el paso anterior tres veces más y realice pasos similares para la siguiente página de ID de elementos.

Ahora, agregar un nuevo elemento a la parte superior aún genera dos solicitudes (una para ID y la otra para obtener detalles de este nuevo elemento). Pero no tendremos que solicitar la próxima página por un tiempo, porque las páginas son más grandes. ¡Tampoco necesitaremos solicitar detalles del artículo porque están en caché!

Pequeño descargo de responsabilidad: la forma en que la IU modera la solicitud de detalles del artículo puede ser más interesante. Por ejemplo, puede omitir solicitudes de algunos elementos si el usuario se desplaza demasiado rápido, porque no está interesado en ellos. Pero esto merece un artículo completamente diferente.

Conclusión

Las soluciones generales generalmente no están optimizadas para casos particulares. Conocer los detalles puede ayudarnos a escribir aplicaciones más rápidas y más optimizadas. Para este caso, el conocimiento era crucial de que el contenido cambia con frecuencia y que se trata de una colección de elementos con ID. Revisemos todas las mejoras que trajo el nuevo enfoque:

  1. Solicitar el orden de los elementos por separado nos permite almacenar en caché los detalles, a pesar de que tuvimos que escribir un caché HTTP modificado.
  2. El almacenamiento en caché de los detalles del elemento da como resultado un uso reducido del ancho de banda.
  3. El aumento del tamaño de las páginas para la solicitud de pedido reduce la cantidad de llamadas.

Una última cosa: las optimizaciones son increíbles, y me pareció emocionante escribir código eficiente, pero no olvides primero perfilarlo. La optimización prematura podría causar problemas y todos deberíamos evitarla.

Gracias por tu tiempo leyendo este artículo. Si te gusta, no olvides hacer clic en a continuación.