ホームページ  >  記事  >  ウェブフロントエンド  >  Fastify と Redis Cache を使用して Web サイトを高速化する

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

WBOY
WBOYオリジナル
2024-08-26 21:46:32367ブラウズ

Speeding Up Your Website Using Fastify and Redis Cache

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

Cloudflareのキャッシュの問題

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

  • 応答のキャッシュを有効にすると、ページ ナビゲーションが壊れました。この件については少し前に Remix フォーラムで問題を提起しましたが、これを書いている時点ではまだ未解決です。応答のキャッシュによってページ ナビゲーションが中断される理由は明らかではありませんが、応答が Cloudflare によってキャッシュされている場合にのみ発生します。
  • 元の投稿で説明されているように、再検証中に古いコンテンツを提供するように Cloudflare を実行させることができませんでした。利用できる機能ではないようです。

他にもいくつか問題が発生しました (パターン マッチングを使用してキャッシュをパージできないなど) が、それらは私の使用例にとって重要ではありませんでした。

そこで、Redis を使用してロジックを Fastify ミドルウェアに移動することにしました。

[!NOTE]
画像キャッシュのために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)。
    • 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 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 Cache を使用して Web サイトを高速化するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。