Hoy publicamos una traducción de la tercera parte de una serie de materiales sobre aceleración instagram.com. En la
primera parte, hablamos sobre la carga previa de datos, en la
segunda , sobre el envío de datos al cliente por iniciativa del servidor. Esto se trata de almacenamiento en caché.

El trabajo comienza con un caché
Ya estamos enviando datos a la aplicación cliente, haciéndolo durante la carga de la página lo antes posible. Esto significa que la única forma más rápida de entregar datos sería aquella que no proporciona los pasos necesarios para solicitar información de un cliente o enviarla a un cliente por iniciativa del servidor. Esto se puede hacer utilizando este enfoque para la formación de páginas, en el que el caché se destaca. Sin embargo, esto significa que tendremos que, aunque de manera muy breve, mostrar al usuario información desactualizada. Con este enfoque, después de cargar la página, enseñamos de inmediato al usuario una copia en caché de su feed e historias, y luego, una vez que los últimos datos están disponibles, reemplazamos todo esto con dichos datos.
Usamos Redux para administrar el estado de instagram.com. Como resultado, el plan de implementación general del esquema anterior se ve así. Almacenamos un subconjunto del repositorio de Redux en el cliente, en la tabla indexedDB, rellenando ese repositorio la primera vez que se carga la página. Sin embargo, trabajar con indexedDB, descargar datos del servidor y la interacción del usuario con la página son procesos asincrónicos. Como resultado, podemos encontrarnos con problemas. Consisten en el hecho de que el usuario está trabajando con el antiguo estado almacenado en caché, y debemos hacer que las acciones del usuario se apliquen al nuevo estado al recibirlo del servidor.
Por ejemplo, si usamos los mecanismos estándar para trabajar con el caché, podemos encontrar el siguiente problema. Estamos comenzando la carga paralela de datos desde el caché y desde la red. Dado que los datos del caché estarán listos más rápido que los datos de la red, se los mostramos al usuario. Entonces, por ejemplo, al usuario le gusta la publicación, pero después de que la respuesta del servidor, que lleva la información más reciente, llega al cliente, esta información sobrescribe la información sobre la publicación que le gusta. En estos nuevos datos no habrá información sobre los gustos que el usuario ha configurado la versión en caché de la publicación. Así es como se ve.
Estado de la carrera que ocurre cuando un usuario interactúa con los datos en caché (las acciones de Redux se resaltan en verde, el estado es gris)Para resolver este problema, necesitábamos cambiar el estado almacenado en caché de acuerdo con las acciones del usuario y guardar información sobre estas acciones, lo que nos permitiría reproducirlas tal como se aplican al nuevo estado recibido del servidor. Si alguna vez ha usado Git u otro sistema de control de versiones, esta tarea puede parecerle familiar. Suponga que el estado en caché de la cinta es la rama del repositorio local, y la respuesta del servidor con los últimos datos es la rama maestra. Si es así, podemos decir que queremos realizar la operación de reubicación, es decir, queremos tomar los cambios registrados en una rama (por ejemplo, me gusta, comentarios, etc.) y aplicarlos a otra.
Esta idea nos lleva a la siguiente arquitectura del sistema:
- Al cargar la página, enviamos una solicitud al servidor para descargar nuevos datos (o esperamos que se envíen por iniciativa del servidor).
- Cree un subconjunto intermedio (en etapas) del estado Redux.
- En el proceso de esperar datos del servidor, guardamos las acciones enviadas.
- Después de recibir datos del servidor, realizamos acciones con los nuevos datos y reproducimos las acciones almacenadas en los nuevos datos, aplicándolos al estado intermedio.
- Después de eso, confirmamos los cambios y reemplazamos el estado actual por uno intermedio.
Resolver un problema causado por una condición de carrera utilizando un estado intermedio (las acciones de Redux se resaltan en verde, el estado es gris)Gracias al estado intermedio, podemos reutilizar todos los reductores existentes. Esto, además, le permite almacenar un estado intermedio (que contiene los últimos datos) por separado del estado actual. Y, dado que el trabajo con el estado intermedio se implementa usando Redux, ¡es suficiente que enviemos acciones para usar este estado!
API
La API de estado intermedio consta de dos funciones principales. Esto es
stagingAction
y
stagingCommit
:
function stagingAction( key: string, promise: Promise<Action>, ): AsyncAction<State, Action> function stagingCommit(key: string): AsyncAction<State, Action>
Hay varias otras funciones allí, por ejemplo, para cancelar cambios y para manejar casos fronterizos, pero no los consideraremos aquí.
La función
stagingAction
acepta una promesa que resuelve un evento que debe enviarse a un estado intermedio. Esta función inicializa un estado intermedio y monitorea las acciones que se han enviado desde su inicialización. Si comparamos esto con los sistemas de control de versiones, resulta que estamos tratando con la creación de una sucursal local. Las acciones en curso se pondrán en cola y se aplicarán al estado intermedio después de que lleguen los nuevos datos.
La función
stagingCommit
reemplaza el estado actual por uno intermedio. Además, si se espera que se completen algunas operaciones asincrónicas que se realizan en un estado intermedio, el sistema esperará a que se completen estas operaciones antes de reemplazarlas. Esto es similar a la operación de reubicación cuando los cambios locales (desde la rama que almacena el caché) se aplican sobre la rama maestra (sobre los nuevos datos recibidos del servidor), lo que lleva al hecho de que la versión local del estado está actualizada.
Para habilitar el sistema de trabajo con un estado intermedio, envolvimos el reductor raíz en las capacidades del extensor del reductor. Procesa la acción
stagingCommit
y aplica acciones guardadas previamente al nuevo estado. Para aprovechar todo esto, solo necesitamos enviar acciones, y todo lo demás se hará automáticamente. Por ejemplo, si queremos cargar una nueva cinta y llevar sus datos a un estado intermedio, podemos hacer algo como esto:
function fetchAndStageFeed() { return stagingAction( 'feed', (async () => { const {data} = await fetchFeedTimeline(); return { type: FEED_LOADED, ...data, }; })(), ); } // store.dispatch(fetchAndStageFeed()); // , stagingCommit, // 'feed' // store.dispatch(stagingCommit('feed'));
El uso de un enfoque de representación para el feed y las historias, en el que el caché se destaca, ha acelerado la producción de materiales en un 2.5% y 11%, respectivamente. Esto, además, contribuyó al hecho de que, en la percepción de los usuarios, la versión web del sistema se acercaba a los clientes de Instagram para iOS y Android.
Estimados lectores! ¿Utiliza algún enfoque para optimizar el almacenamiento en caché cuando trabaja en sus proyectos?
