"Si un ouvrier veut bien faire son travail, il doit d'abord affûter ses outils." - Confucius, "Les Entretiens de Confucius. Lu Linggong"
Page de garde > La programmation > Accélérer votre site Web à l'aide de Fastify et Redis Cache

Accélérer votre site Web à l'aide de Fastify et Redis Cache

Publié le 2024-08-27
Parcourir:761

Speeding Up Your Website Using Fastify and Redis Cache

Il y a moins de 24 heures, j'ai écrit un article sur la façon d'accélérer votre site Web à l'aide du cache Cloudflare. Cependant, j'ai depuis déplacé l'essentiel de la logique vers un middleware Fastify utilisant Redis. Voici pourquoi et comment vous pouvez le faire vous-même.

Problèmes de cache Cloudflare

J'ai rencontré deux problèmes avec le cache Cloudflare :

  • La navigation dans les pages s'est interrompue après l'activation de la mise en cache des réponses. J'ai soulevé un problème à ce sujet sur le forum Remix il y a quelque temps, mais au moment d'écrire ces lignes, il n'est toujours pas résolu. On ne sait pas pourquoi la mise en cache de la réponse entraîne une interruption de la navigation dans les pages, mais cela ne se produit que lorsque la réponse est mise en cache par Cloudflare.
  • Je n'ai pas réussi à faire en sorte que Cloudflare exécute le service de contenu obsolète lors de la revalidation comme décrit dans le message d'origine. Il semble que cette fonctionnalité ne soit pas disponible.

J'ai rencontré quelques autres problèmes (comme ne pas pouvoir purger le cache à l'aide de la correspondance de modèles), mais ceux-ci n'étaient pas critiques pour mon cas d'utilisation.

Par conséquent, j'ai décidé de déplacer la logique vers un middleware Fastify utilisant Redis.

[!NOTER]
J'ai quitté le cache Cloudflare pour la mise en cache des images. Dans ce cas, le cache Cloudflare fonctionne effectivement comme un CDN.

Fastifier le middleware

Ce qui suit est une version annotée du middleware que j'ai écrit pour mettre en cache les réponses à l'aide de 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;
});

Les commentaires parcourent le code, mais voici quelques points clés :

  • Critères de mise en cache :
    • Demandes :
    • Ne mettez pas en cache les réponses des utilisateurs authentifiés.
    • Cache uniquement les requêtes GET.
    • Cache uniquement les réponses pour les URL qui incluent "/supplements/".
    • Contourner le cache si l'en-tête de la requête contient cache-control : no-cache.
    • Réponses :
    • Cache uniquement les réponses réussies (statusCode est 200).
    • Ne mettez pas en cache les réponses déjà servies à partir du cache (x-pillser-cache : HIT).
    • Mettre en cache uniquement les réponses avec le type de contenu : text/html.
  • Génération de clé de cache :
    • Utilisez le hachage SHA-256 d'une représentation JSON contenant la méthode de requête, l'URL et la largeur de la fenêtre d'affichage.
    • Préfixez la clé de cache avec « request : » pour faciliter l'espacement des noms et la purge.
  • Traitement des demandes :
    • Accédez au cycle de vie onRequest pour vérifier si une requête a une réponse en cache.
    • Servez la réponse mise en cache si elle est disponible, en la marquant avec x-pillser-cache : HIT.
    • Démarrez une tâche en arrière-plan pour actualiser le cache après l'envoi d'une réponse mise en cache, en implémentant « Servir le contenu obsolète lors de la revalidation ».
  • Gestion des réponses :
    • Accédez au cycle de vie onSend pour traiter et mettre en cache les réponses.
    • Convertissez les flux lisibles en chaîne pour une mise en cache plus simple.
    • Exclure les en-têtes spécifiques (content-length, set-cookie, x-pillser-cache) du cache.
    • Marquer les réponses non mises en cache comme x-pillser-cache : DYNAMIC.
    • Réponses en cache avec un TTL (Time To Live) d'un jour, marquant les nouvelles entrées avec x-pillser-cache : MISS.

Résultats

J'ai effectué des tests de latence à partir de plusieurs emplacements et capturé le temps de réponse le plus lent pour chaque URL. Les résultats sont ci-dessous :

URL Pays Temps de réponse d'origine Temps de réponse mis en cache par Cloudflare Fastifier le temps de réponse mis en cache
https://pillser.com/vitamins/vitamin-b1 us-west1 240 ms 16 ms 40 ms
https://pillser.com/vitamins/vitamin-b1 europe-ouest3 320 ms 10 ms 110 ms
https://pillser.com/vitamins/vitamin-b1 Australie-sud-est1 362 ms 16 ms 192 ms
https://pillser.com/supplements/vitamin-b1-3254 us-west1 280 ms 10 ms 38 ms
https://pillser.com/supplements/vitamin-b1-3254 europe-ouest3 340 ms 12 ms 141 ms
https://pillser.com/supplements/vitamin-b1-3254 Australie-sud-est1 362 ms 14 ms 183 ms

Par rapport au cache Cloudflare, le cache Fastify est plus lent. En effet, le contenu mis en cache est toujours servi depuis l'origine, tandis que le cache Cloudflare est servi depuis des emplacements périphériques régionaux. Cependant, j'ai trouvé que ces temps de réponse sont suffisants pour obtenir une bonne expérience utilisateur.

Déclaration de sortie Cet article est reproduit sur : https://dev.to/lilouartz/speeding-up-your-website-using-fastify-and-redis-cache-4ck6?1 En cas de violation, veuillez contacter [email protected] pour le supprimer
Dernier tutoriel Plus>

Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.

Copyright© 2022 湘ICP备2022001581号-3