首頁 > web前端 > js教程 > 主體

使用 Fastify 和 Redis 快取加速您的網站

WBOY
發布: 2024-08-26 21:46:32
原創
276 人瀏覽過

Speeding Up Your Website Using Fastify and Redis Cache

不到 24 小時前,我寫了一篇關於如何使用 Cloudflare 快取加速您的網站的文章。不過,我已經將大部分邏輯轉移到使用 Redis 的 Fastify 中間件。以下是您自己操作的原因和方法。

Cloudflare 快取問題

我遇到了 Cloudflare 快取的兩個問題:

  • 啟用回應快取後頁面導航中斷。不久前我在 Remix 論壇上提出了一個有關此問題的問題,但截至撰寫本文時,該問題仍未解決。目前尚不清楚為什麼快取回應會導致頁面導航中斷,但只有當 Cloudflare 快取回應時才會發生這種情況。
  • 我無法讓 Cloudflare 按照原始帖子中的描述執行重新驗證時提供陳舊內容。看起來這不是可用的功能。

我還遇到了一些其他問題(例如無法使用模式匹配清除快取),但這些對我的用例來說並不重要。

因此,我決定將邏輯轉移到使用 Redis 的 Fastify 中間件。

[!注意]
我將 Cloudflare 快取留給映像快取。在這種情況下,Cloudflare 快取有效地充當 CDN。

Fastify 中介軟體

以下是我使用 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<string, string>;
  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<string> => {
  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<string, string>;

  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;
});
登入後複製

註解貫穿了程式碼,但這裡有一些關鍵點:

  • 快取標準:
    • 請求:
    • 不要快取經過驗證的使用者的回應。
    • 僅快取 GET 請求。
    • 僅快取包含「/supplements/」的 URL 的回應。
    • 如果請求頭包含cache-control: no-cache,則繞過快取。
    • 回覆:
    • 僅快取成功的回應(statusCode 為 200)。
    • 不要快取已經從快取提供的回應(x-pillser-cache:HIT)。
    • 僅快取內容類型為:text/html 的回應。
  • 快取金鑰產生:
    • 使用包含請求方法、URL 和視口寬度的 JSON 表示形式的 SHA-256 雜湊。
    • 在快取鍵前加上「request:」前綴,以便於命名空間和清除。
  • 請求處理:
    • 掛鉤 onRequest 生命週期以檢查請求是否有快取的回應。
    • 提供快取的回應(如果可用),並使用 x-pillser-cache: HIT 進行標記。
    • 發送快取回應後啟動後台任務刷新緩存,實現「重新驗證時提供陳舊內容」。
  • 響應處理:
    • 掛鉤 onSend 生命週期來處理和快取回應。
    • 將可讀流轉換為字串以簡化快取。
    • 從快取中排除特定標頭(content-length、set-cookie、x-pillser-cache)。
    • 將不可緩存的回應標記為 x-pillser-cache: DYNAMIC。
    • 以一天的 TTL(生存時間)快取回應,用 x-pillser-cache: MISS 標記新條目。

結果

我從多個位置運行了延遲測試,並捕獲了每個 URL 的最慢響應時間。結果如下:

URL Country Origin Response Time Cloudflare Cached Response Time Fastify Cached Response Time
https://pillser.com/vitamins/vitamin-b1 us-west1 240ms 16ms 40ms
https://pillser.com/vitamins/vitamin-b1 europe-west3 320ms 10ms 110ms
https://pillser.com/vitamins/vitamin-b1 australia-southeast1 362ms 16ms 192ms
https://pillser.com/supplements/vitamin-b1-3254 us-west1 280ms 10ms 38ms
https://pillser.com/supplements/vitamin-b1-3254 europe-west3 340ms 12ms 141ms
https://pillser.com/supplements/vitamin-b1-3254 australia-southeast1 362ms 14ms 183ms

與 Cloudflare 快取相比,Fastify 快取速度較慢。這是因為快取的內容仍然是從來源提供的,而 Cloudflare 快取是從區域邊緣位置提供的。然而,我發現這些回應時間足以實現良好的使用者體驗。

以上是使用 Fastify 和 Redis 快取加速您的網站的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!