「労働者が自分の仕事をうまくやりたいなら、まず自分の道具を研ぎ澄まさなければなりません。」 - 孔子、「論語。陸霊公」
表紙 > プログラミング > Fastify と Redis Cache を使用して Web サイトを高速化する

Fastify と Redis Cache を使用して Web サイトを高速化する

2024 年 8 月 27 日に公開
ブラウズ:413

Speeding Up Your Website Using Fastify and Redis Cache

24 時間も前に、Cloudflare キャッシュを使用して Web サイトを高速化する方法についての投稿を書きました。ただし、その後、ロジックの大部分を Redis を使用する Fastify ミドルウェアに移行しました。その理由と、自分で行う方法は次のとおりです。

Cloudflareのキャッシュの問題

Cloudflare キャッシュで 2 つの問題が発生しました:

  • 応答のキャッシュを有効にするとページ ナビゲーションが壊れました。この件については少し前に 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;
  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;
});

コメントはコードを詳しく説明していますが、重要なポイントがいくつかあります:

  • キャッシュ基準:
    • リクエスト:
    • 認証されたユーザーの応答をキャッシュしません。
    • GET リクエストのみをキャッシュします。
    • 「/supplements/」を含む URL の応答のみをキャッシュします。
    • リクエストヘッダーにcache-control: no-cacheが含まれている場合はキャッシュをバイパスします。
    • 応答:
    • 成功した応答のみをキャッシュします (statusCode は 200)。
    • すでにキャッシュから提供されている応答をキャッシュしません (x-pillser-cache: HIT)。
    • content-type: text/html の応答のみをキャッシュします。
  • キャッシュキーの生成:
    • リクエスト メソッド、URL、ビューポート幅を含む JSON 表現の SHA-256 ハッシュを使用します。
    • 名前空間の設定と削除を容易にするために、キャッシュ キーの先頭に「request:」を付けます。
  • リクエストの処理:
    • onRequest ライフサイクルにフックして、リクエストにキャッシュされた応答があるかどうかを確認します。
    • 利用可能な場合はキャッシュされた応答を提供し、x-pillser-cache: HIT でマークします。
    • キャッシュされた応答を送信した後にバックグラウンド タスクを開始してキャッシュを更新し、「再検証中に古いコンテンツを提供する」を実装します。
  • 応答処理:
    • onSend ライフサイクルにフックして、応答を処理してキャッシュします。
    • キャッシュを簡素化するために、読み取り可能なストリームを文字列に変換します。
    • 特定のヘッダー (content-length、set-cookie、x-pillser-cache) をキャッシュから除外します。
    • キャッシュ不可能な応答を x-pillser-cache としてマークします: DYNAMIC.
    • 1 日の TTL (Time To Live) で応答をキャッシュし、x-pillser-cache: MISS で新しいエントリをマークします。

結果

いくつかの場所からレイテンシ テストを実行し、各 URL の最も遅い応答時間を取得しました。結果は以下の通りです:

URL オリジン応答時間 Cloudflareのキャッシュされた応答時間 キャッシュされた応答時間の高速化
https://pillser.com/vitamins/vitamin-b1 us-west1 240ミリ秒 16ミリ秒 40ミリ秒
https://pillser.com/vitamins/vitamin-b1 ヨーロッパ西3 320ミリ秒 10ミリ秒 110ミリ秒
https://pillser.com/vitamins/vitamin-b1 オーストラリア南東1 362ミリ秒 16ミリ秒 192ミリ秒
https://pillser.com/supplements/vitamin-b1-3254 us-west1 280ミリ秒 10ミリ秒 38ミリ秒
https://pillser.com/supplements/vitamin-b1-3254 ヨーロッパ西3 340ミリ秒 12ミリ秒 141ミリ秒
https://pillser.com/supplements/vitamin-b1-3254 オーストラリア南東1 362ミリ秒 14ミリ秒 183ミリ秒

Cloudflare キャッシュと比較すると、Fastify キャッシュは低速です。これは、Cloudflare キャッシュが地域のエッジロケーションから提供されるのに対し、キャッシュされたコンテンツは引き続きオリジンから提供されるためです。ただし、優れたユーザー エクスペリエンスを実現するには、これらの応答時間で十分であることがわかりました。

リリースステートメント この記事は次の場所に転載されています: https://dev.to/lilouartz/speeding-up-your-website-using-fastify-and-redis-cache-4ck6?1 侵害がある場合は、[email protected] までご連絡ください。それを削除するには
最新のチュートリアル もっと>

免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。

Copyright© 2022 湘ICP备2022001581号-3