"Si un trabajador quiere hacer bien su trabajo, primero debe afilar sus herramientas." - Confucio, "Las Analectas de Confucio. Lu Linggong"
Página delantera > Programación > Acelerar su sitio web usando Fastify y Redis Cache

Acelerar su sitio web usando Fastify y Redis Cache

Publicado el 2024-08-27
Navegar:815

Speeding Up Your Website Using Fastify and Redis Cache

Hace menos de 24 horas, escribí una publicación sobre cómo acelerar su sitio web usando el caché de Cloudflare. Sin embargo, desde entonces moví la mayor parte de la lógica a un middleware Fastify usando Redis. Aquí te explicamos por qué y cómo puedes hacerlo tú mismo.

Problemas de caché de Cloudflare

Me encontré con dos problemas con el caché de Cloudflare:

  • La navegación de la página se interrumpió después de habilitar el almacenamiento en caché de las respuestas. Planteé un problema sobre esto en el foro de Remix hace un tiempo, pero al momento de escribir esto, aún no se ha resuelto. No está claro por qué el almacenamiento en caché de la respuesta provoca que se interrumpa la navegación de la página, pero solo ocurre cuando Cloudflare almacena en caché la respuesta.
  • No pude lograr que Cloudflare realizara Servir contenido obsoleto mientras se revalida como se describe en la publicación original. Parece que no es una función disponible.

Hubo algunos otros problemas con los que me encontré (como no poder purgar el caché usando la coincidencia de patrones), pero no fueron críticos para mi caso de uso.

Por lo tanto, decidí mover la lógica a un middleware Fastify usando Redis.

[!NOTA]
Dejé el caché de Cloudflare para el almacenamiento en caché de imágenes. En este caso, la caché de Cloudflare funciona efectivamente como una CDN.

Fastificar el middleware

Lo que sigue es una versión anotada del middleware que escribí para almacenar en caché las respuestas usando Fastify.

const isCacheableRequest = (request: FastifyRequest): boolean => {
  // Do not attempt to use cache for authenticated visitors.
  if (request.visitor?.userAccount) {
    return false;
  }

  if (request.method !== 'GET') {
    return false;
  }

  // We only want to cache responses under /supplements/.
  if (!request.url.includes('/supplements/')) {
    return false;
  }

  // We provide a mechanism to bypass the cache.
  // This is necessary for implementing the "Serve Stale Content While Revalidating" feature.
  if (request.headers['cache-control'] === 'no-cache') {
    return false;
  }

  return true;
};

const isCacheableResponse = (reply: FastifyReply): boolean => {
  if (reply.statusCode !== 200) {
    return false;
  }

  // We don't want to cache responses that are served from the cache.
  if (reply.getHeader('x-pillser-cache') === 'HIT') {
    return false;
  }

  // We only want to cache responses that are HTML.
  if (!reply.getHeader('content-type')?.toString().includes('text/html')) {
    return false;
  }

  return true;
};

const generateRequestCacheKey = (request: FastifyRequest): string => {
  // We need to namespace the cache key to allow an easy purging of all the cache entries.
  return 'request:'   generateHash({
    algorithm: 'sha256',
    buffer: stringifyJson({
      method: request.method,
      url: request.url,
      // This is used to cache viewport specific responses.
      viewportWidth: request.viewportWidth,
    }),
    encoding: 'hex',
  });
};

type CachedResponse = {
  body: string;
  headers: Record;
  statusCode: number;
};

const refreshRequestCache = async (request: FastifyRequest) => {
  await got({
    headers: {
      'cache-control': 'no-cache',
      'sec-ch-viewport-width': String(request.viewportWidth),
      'user-agent': request.headers['user-agent'],
    },
    method: 'GET',
    url: pathToAbsoluteUrl(request.originalUrl),
  });
};

app.addHook('onRequest', async (request, reply) => {
  if (!isCacheableRequest(request)) {
    return;
  }

  const cachedResponse = await redis.get(generateRequestCacheKey(request));

  if (!cachedResponse) {
    return;
  }

  reply.header('x-pillser-cache', 'HIT');

  const response: CachedResponse = parseJson(cachedResponse);

  reply.status(response.statusCode);
  reply.headers(response.headers);
  reply.send(response.body);
  reply.hijack();

  setImmediate(() => {
    // After the response is sent, we send a request to refresh the cache in the background.
    // This effectively serves stale content while revalidating.
    // Therefore, this cache does not reduce the number of requests to the origin;
    // The goal is to reduce the response time for the user.
    refreshRequestCache(request);
  });
});

const readableToString = (readable: Readable): Promise => {
  const chunks: Uint8Array[] = [];

  return new Promise((resolve, reject) => {
    readable.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
    readable.on('error', (err) => reject(err));
    readable.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
  });
};

app.addHook('onSend', async (request, reply, payload) => {
  if (reply.hasHeader('x-pillser-cache')) {
    return payload;
  }

  if (!isCacheableRequest(request) || !isCacheableResponse(reply) || !(payload instanceof Readable)) {
    // Indicate that the response is not cacheable.
    reply.header('x-pillser-cache', 'DYNAMIC');

    return payload;
  }

  const content = await readableToString(payload);

  const headers = omit(reply.getHeaders(), [
    'content-length',
    'set-cookie',
    'x-pillser-cache',
  ]) as Record;

  reply.header('x-pillser-cache', 'MISS');

  await redis.setex(
    generateRequestCacheKey(request),
    getDuration('1 day', 'seconds'),
    stringifyJson({
      body: content,
      headers,
      statusCode: reply.statusCode,
    } satisfies CachedResponse),
  );

  return content;
});

Los comentarios recorren el código, pero aquí hay algunos puntos clave:

  • Criterios de almacenamiento en caché:
    • Solicitudes:
    • No almacenar en caché las respuestas de los usuarios autenticados.
    • Solo caché de solicitudes GET.
    • Solo almacena en caché las respuestas para las URL que incluyen "/supplements/".
    • Omita el caché si el encabezado de la solicitud contiene control de caché: no-cache.
    • Respuestas:
    • Solo almacena en caché las respuestas exitosas (el código de estado es 200).
    • No almacenar en caché las respuestas ya entregadas desde el caché (x-pillser-cache: HIT).
    • Solo almacena en caché las respuestas con tipo de contenido: texto/html.
  • Generación de claves de caché:
    • Utilice el hash SHA-256 de una representación JSON que contenga el método de solicitud, la URL y el ancho de la ventana gráfica.
    • Prefiera la clave de caché con 'solicitud:' para facilitar el espacio de nombres y la eliminación.
  • Manejo de solicitudes:
    • Conéctese al ciclo de vida de onRequest para verificar si una solicitud tiene una respuesta almacenada en caché.
    • Entregue la respuesta almacenada en caché si está disponible, marcándola con x-pillser-cache: HIT.
    • Iniciar una tarea en segundo plano para actualizar el caché después de enviar una respuesta almacenada en caché, implementando "Servir contenido obsoleto mientras se revalida".
  • Manejo de respuestas:
    • Conéctese al ciclo de vida de onSend para procesar y almacenar en caché las respuestas.
    • Convierta secuencias legibles en cadenas para un almacenamiento en caché más sencillo.
    • Excluir encabezados específicos (longitud del contenido, set-cookie, x-pillser-cache) del caché.
    • Marcar respuestas que no se pueden almacenar en caché como x-pillser-cache: DINÁMICO.
    • Caché de respuestas con un TTL (Time To Live) de un día, marcando nuevas entradas con x-pillser-cache: MISS.

Resultados

Realicé pruebas de latencia desde varias ubicaciones y capturé el tiempo de respuesta más lento para cada URL. Los resultados están a continuación:

URL País Tiempo de respuesta de origen Tiempo de respuesta en caché de Cloudflare Acelerar el tiempo de respuesta en caché
https://pillser.com/vitamins/vitamin-b1 nosotros-oeste1 240 ms 16 ms 40 ms
https://pillser.com/vitamins/vitamin-b1 europa-oeste3 320 ms 10 ms 110 ms
https://pillser.com/vitamins/vitamin-b1 australia-sureste1 362 ms 16 ms 192ms
https://pillser.com/supplements/vitamin-b1-3254 nosotros-oeste1 280 ms 10 ms 38 ms
https://pillser.com/supplements/vitamin-b1-3254 europa-oeste3 340 ms 12 ms 141ms
https://pillser.com/supplements/vitamin-b1-3254 australia-sureste1 362 ms 14 ms 183ms

En comparación con el caché de Cloudflare, el caché de Fastify es más lento. Esto se debe a que el contenido almacenado en caché todavía se sirve desde el origen, mientras que el caché de Cloudflare se sirve desde ubicaciones perimetrales regionales. Sin embargo, descubrí que estos tiempos de respuesta son suficientes para lograr una buena experiencia de usuario.

Declaración de liberación Este artículo se reproduce en: https://dev.to/lilouartz/speeding-up-your-website-using-fastify-and-redis-cache-4ck6?1 Si hay alguna infracción, comuníquese con [email protected] para borrarlo
Último tutorial Más>

Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.

Copyright© 2022 湘ICP备2022001581号-3