」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > 使用 Fastify 和 Redis 快取加速您的網站

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

發佈於2024-08-27
瀏覽:578

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;
  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)。
    • 仅缓存内容类型为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 的最慢响应时间。结果如下:

网址 国家 原点响应时间 Cloudflare 缓存响应时间 Fastify 缓存响应时间
https://pilser.com/vitamins/vitamin-b1 us-west1 240ms 16ms 40ms
https://pilser.com/vitamins/vitamin-b1 欧洲西部3 320ms 10ms 110ms
https://pilser.com/vitamins/vitamin-b1 澳大利亚-东南部1 362ms 16ms 192ms
https://pilser.com/supplements/vitamin-b1-3254 us-west1 280ms 10ms 38ms
https://pilser.com/supplements/vitamin-b1-3254 欧洲西部3 340ms 12ms 141ms
https://pilser.com/supplements/vitamin-b1-3254 澳大利亚-东南部1 362ms 14ms 183ms

与 Cloudflare 缓存相比,Fastify 缓存速度较慢。这是因为缓存的内容仍然从源提供服务,而 Cloudflare 缓存则从区域边缘位置提供服务。然而,我发现这些响应时间足以实现良好的用户体验。

版本聲明 本文轉載於:https://dev.to/lilouartz/speeding-up-your-website-using-fastify-and-redis-cache-4ck6?1如有侵犯,請聯絡[email protected]刪除
最新教學 更多>
  • 如何解決 PHPmailer HTML 內容渲染問題?
    如何解決 PHPmailer HTML 內容渲染問題?
    PHPmailer 無法渲染HTML 內容使用PHPmailer 發送電子郵件時,使用者遇到HTML 程式碼顯示為原始文字的問題交貨時。儘管使用了 IsHTML() 方法,所需的 HTML 內容仍然無法存取。 潛在問題此行為背後的原因在於方法呼叫的順序。與它的前身不同,PHPMailer 6 要求在...
    程式設計 發佈於2024-11-06
  • 如何使用 Java 從 HTML 文件中提取資料?
    如何使用 Java 從 HTML 文件中提取資料?
    Java HTML解析要從網站取得數據,首先必須了解HTML文件的結構。 HTML 元素使用標籤進行組織,標籤指定每個元素的類型和內容。 例如,以下HTML 表示具有特定CSS 類別的div 標籤:<div class="classname"></div>...
    程式設計 發佈於2024-11-06
  • 為什麼 Java 異常處理程式碼會產生「132Exception in thread main MyExc1」而不是「13Exception in thread main MyExc2」?
    為什麼 Java 異常處理程式碼會產生「132Exception in thread main MyExc1」而不是「13Exception in thread main MyExc2」?
    Java中的異常處理:解開歧義Java中的異常處理:解開歧義// Exception Heirarchy class MyExc1 extends Exception {} class MyExc2 extends Exception {} class MyExc3 extends MyExc2 {...
    程式設計 發佈於2024-11-06
  • 從 shell 腳本遷移到“Bun 腳本”
    從 shell 腳本遷移到“Bun 腳本”
    在 zCloud 從事專注於流程自動化和基礎設施的專案時,我們經常遇到需要建立多個函數來執行驗證和通用流程的情況。僅使用一種作業系統時一切正常,但當涉及多個系統時情況會變得複雜。 在我們的例子中,大部分開發都發生在 Linux 上,但我們也需要確保與 macOS 的兼容性。這通常會導致程式碼不相容...
    程式設計 發佈於2024-11-06
  • 您的 Web 專案中 jQuery 函式庫的最佳來源在哪裡?
    您的 Web 專案中 jQuery 函式庫的最佳來源在哪裡?
    您應該從哪裡取得 jQuery 函式庫? 當您的專案包含 jQuery 和 jQuery UI 時,有多個選項可用。讓我們深入研究一下每種方法的優缺點。 Google JSAPI 與 CDNGoogle JSAPI 提供了一種從 Google 分散式伺服器存取 jQuery 的便捷方法。這可以縮短載...
    程式設計 發佈於2024-11-06
  • PHP 設計模式:轉接器
    PHP 設計模式:轉接器
    適配器設計模式是一種結構模式,允許具有不相容介面的物件一起工作。它充當兩個物件之間的中介(或適配​​器),將一個物件的介面轉換為另一個物件期望的介面。這允許那些因為具有不同介面而不相容的類別在不修改其原始程式碼的情況下進行協作。 適配器結構 適配器模式一般由三個主要元素組成: 客戶端:期望與特定介...
    程式設計 發佈於2024-11-06
  • 了解 PHP 中的 WebSocket
    了解 PHP 中的 WebSocket
    WebSockets 通过单个 TCP 连接提供实时、全双工通信通道。与 HTTP 不同,HTTP 中客户端向服务器发送请求并等待响应,WebSocket 允许客户端和服务器之间进行连续通信,而无需多次请求。这非常适合需要实时更新的应用程序,例如聊天应用程序、实时通知和在线游戏。 在本指南中,我们将...
    程式設計 發佈於2024-11-06
  • Visual Studio 2012 支援哪些 C++11 功能?
    Visual Studio 2012 支援哪些 C++11 功能?
    Visual Studio 2012 中的 C 11 功能隨著最近發布的 Visual Studio 2012 預覽版,許多開發人員對 C 11 功能的支援感到好奇。雖然 Visual Studio 2010 已提供部分 C 11 支持,但新版本提供了擴充的功能。 Visual Studio 201...
    程式設計 發佈於2024-11-06
  • 如何在Windows啟動時自動執行Python腳本?
    如何在Windows啟動時自動執行Python腳本?
    在 Windows 啟動時運行 Python 腳本每次 Windows 啟動時執行 Python 腳本對於自動化任務或啟動基本程式至關重要。多種方法提供不同等級的自訂和使用者控制。 自動執行腳本的選項:1。打包為服務:建立 Windows 服務並安裝它。此方法在電腦上運行腳本,無論使用者是否登入。需...
    程式設計 發佈於2024-11-06
  • 探索 Astral.CSS:徹底改變網頁設計的 CSS 框架。
    探索 Astral.CSS:徹底改變網頁設計的 CSS 框架。
    在快節奏的 Web 開發世界中,框架在幫助開發人員高效創建具有視覺吸引力和功能性的網站方面發揮著關鍵作用。在當今可用的各種框架中,Astral CSS 因其獨特的設計理念和易用性而脫穎而出。本文深入探討了 Astral CSS 的功能、優點和整體影響。 什麼是星界? Astral 是一個現代 C...
    程式設計 發佈於2024-11-06
  • ESnd 箭頭函數綜合指南
    ESnd 箭頭函數綜合指南
    ES6简介 ECMAScript 2015,也称为 ES6 (ECMAScript 6),是对 JavaScript 的重大更新,引入了新的语法和功能,使编码更高效、更易于管理。 JavaScript 是用于 Web 开发的最流行的编程语言之一,ES6 的改进大大增强了其功能。 本...
    程式設計 發佈於2024-11-06
  • 揭示演算法和資料結構:高效程式設計的基礎
    揭示演算法和資料結構:高效程式設計的基礎
    在這一系列文章中,我將分享我的學習歷程,涉及在學術環境和大型科技公司中廣泛討論的兩個主題:演算法和資料結構。儘管這些主題乍看之下似乎令人畏懼,特別是對於像我這樣由於其他職業挑戰而在整個職業生涯中沒有機會深入研究這些主題的人,但我的目標是讓它們易於理解。 我將從最基本的概念開始,然後轉向更高級的主題...
    程式設計 發佈於2024-11-06
  • 如何使用 pprof 來分析 Go 程式中的 goroutine 數量?
    如何使用 pprof 來分析 Go 程式中的 goroutine 數量?
    使用 pprof 分析 Goroutine 數量使用 pprof 分析 Goroutine 數量檢測 Go 程式中潛在的 Goroutine 洩漏需要監控一段時間內活動的 Goroutine 數量。雖然標準 go 工具 pprof 命令提供了對阻塞的深入了解,但它並不直接解決 goroutine 計...
    程式設計 發佈於2024-11-06
  • 如何將類別方法作為回調傳遞:了解機制和技術
    如何將類別方法作為回調傳遞:了解機制和技術
    如何將類別方法作為回調傳遞後台在某些場景下,您可能需要將類別方法作為回調傳遞給其他函數以提高效率具體任務的執行。本文將引導您完成實現此目的的各種機制。 使用可調用語法要將函數作為回調傳遞,您可以直接將其名稱作為字串提供。但是,此方法不適用於類別方法。 傳遞實例方法類別實例方法可以使用陣列作為回調傳遞...
    程式設計 發佈於2024-11-06
  • 網頁抓取 - 有趣!
    網頁抓取 - 有趣!
    一個很酷的術語: CRON = 依指定時間間隔自動安排任務的程式設計技術 網路什麼? 在研究專案等時,我們通常會從各個網站編寫資訊 - 無論是日記/Excel/文件等。 我們正在抓取網路並手動提取資料。 網路抓取正在自動化這個過程。 例子 當在網路上搜尋運動鞋時...
    程式設計 發佈於2024-11-06

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3