[Cloudflare] 一套能让免费 CloudFlare 建的站部分访问延迟变低(几乎瞬间)的方法,不是优选,夸我或者给我找 bug

效果 我让 AI 写的简单页面 LCP 100 ms 左右。首个请求完成也就二三十毫秒。 缺陷 首次访问别想了,该卡还是卡。 部分解决 Edge 上的一个导致延迟大的问题比较复杂。解决方法在后面。 只对可预测性比较强的网站效果更好。可预测性不强的网站想要快代价很可能比较大,具体方法在后面。 如果网站...
[Cloudflare] 一套能让免费 CloudFlare 建的站部分访问延迟变低(几乎瞬间)的方法,不是优选,夸我或者给我找 bug
[Cloudflare] 一套能让免费 CloudFlare 建的站部分访问延迟变低(几乎瞬间)的方法,不是优选,夸我或者给我找 bug

效果

  • 我让 AI 写的简单页面 LCP 100 ms 左右。首个请求完成也就二三十毫秒。

缺陷

  • 首次访问别想了,该卡还是卡。
  • 部分解决 Edge 上的一个导致延迟大的问题比较复杂。解决方法在后面。
  • 只对可预测性比较强的网站效果更好。可预测性不强的网站想要快代价很可能比较大,具体方法在后面。
  • 如果网站更新了,用户刷新两次才能获得最新内容。

代价

  • 比较复杂。
  • 网站有一定概率会彻底崩,而且比较难恢复。
  • 可能会消耗更多的流量。

主要技术

  • Service Worker
  • SWR

方法

简单说就是用 service worker 实现一个 SWR 缓存策略。

真传一句话,下面假传万卷书。

service worker 我理解就是一个运行在浏览器的类似代理的东西,可以任意修改特定网站所有的请求的响应。SWR 我理解就是如果没缓存,就先访问服务器获取内容,然后返回响应;如果有缓存,就直接返回缓存,然后再访问服务器更新缓存。这段我写的时候没有参考任何资料,都是凭记忆写的。在意的话自己搜搜。

虽然看起来挺简单的,但实际实现的时候还是会遇到一些比较恶心的问题。比如如果 service worker 安装过程比较耗时的话,可能导致一些页面无法缓存,最好是在 service worker 的激活事件里显式缓存一下。否则可能导致第三次访问的时候才能快速打开。

缺陷详解

我相信在这种条件下让首次访问也快是不可能的。不过我也没啥根据,基本就是直觉,我倒是希望首次也快。可能不同的网站互相缓存可以部分提升。不过 service worker 是不能跨域的,用 service worker 应该是不行。

在 edge 上就算使用了缓存,五分钟以上不访问之后再次访问还是偶尔会出现一两秒的延迟。解决方法是在所有页面中添加一个隐藏的框架,框架的内容就是网站内的一个页面,比如说 /keepalive ,然后每隔一段时间刷新这个框架的内容。这样如果页面开着的话,基本就不会出现卡顿的情况了。我测试半分钟效果不错。另外在 service worker 中添加一点代码,如果请求的 pathname 部分是 /keepalive ,就直接用代码返回响应,不要经过服务器。我试过了,效果一样。我基本可以确定这种刷新不会导致太多的服务器请求。DNS 请求应该是免不了了,我在 CF 的 DNS 请求页面看了,不访问的时候基本没有 DNS 请求,持续刷新的过程中五分钟十次请求。据说那个十次是假的,不知道真假。如果在 service worker 中直接返回响应的话,理论上应该是不会有 web 服务器流量的。但是由于 cloudflare 好像是没有监控静态页面流量的功能,所以我就不管了。Wireshark 我不太会用,不想学。谁要是能帮我分析一下我谢谢你。我问过微软的客服的,看起来微软没有解决这个问题的意思,微软客服大概说 chrome 用了缓存能瞬间打开是链接复用之类的机制比较激进,edge 没那么激进。我有点怀疑 edge 就是没优先用缓存的 dns 映射(这个词可能不准确),chrome 可能直接用缓存的 dns 记录了。就说是 chrome 可能对 dns 映射用的是 SWR 策略。

我想做的是一个类似解谜游戏的网站,实际不是游戏,具体我不想说。我预测大部分用户都会一关一关访问,所以在首页预载第一关,在第一关预载第二关,以此类推效果应该非常不错。这部分代码里没有。我之前试过,但是没有跟解决 edge 问题的代码一起测试,我估计是没什么问题。如果用户访问行为不好预测的话,并且网站页面不多的话,直接暴力预载所有内容可能也不错。网站内容多的话可能就不行了。预载访问量排前几个的?

如果更新了需要刷新两次才能看到最新内容是 SWR 策略决定的。我试过让 AI 实现检测到更新在页面上显示一个通知,但实际搞下来我感觉太复杂了,放弃了。你要是有勇气可以试试。

代价详解

复杂就不多解释了,前面那么多字应该有点体现了。而且我可能还没有把所有坑都踩遍。

service worker 出问题很危险,可能会导致全站无法访问。而且有一些坑。比如如果 service worker 的脚本浏览器缓存时间比较长的话,用户的浏览器可能无法及时更新。解决方法挺复杂。我建议能跑就尽量别改 service worker 的代码。如果有新的需要改 service worker 的需求,如果价值不大就别实现了,保命要紧。反正我就打算这么干了。

cloudflare 一般情况下好像不会给出完整的响应,而是通过对比一个请求头判断浏览器缓存是否过时了,过时了才发完整的请求。我不知道 SWR 会不会利用这个机制。

弯路

刚开始我是想通过浏览器缓存很长时间的 HTML 加动态加载各个页面对应的 js 文件动态修改页面内容来实现的,后来好像被 AI 提醒 service worker 更好就用了后者。

临时 Demo

https://sdwpub.cc.cd/

过一段时间很可能会失效。

在 edge 上测试方法就是开着首页,再打开 https://sdwpub.cc.cd/test 再关闭,缓存这个页面。然后过一段时间访问 https://sdwpub.cc.cd/test 。我测了很多次都没卡顿。如果不开着首页过五分钟就有可能卡顿。chrome 的话,打开一次首页之后再访问首页就不卡顿了。

代码

index.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Edge 卡顿缓解</title>
</head>
<body>
  <!-- 正常页面内容 -->

  <span>测试。</span>

  <!-- 不可见框架,用于保持页面活跃 -->
  <iframe id="keepAliveFrame" src="keepalive" style="display:none;"></iframe>

  <script>
    // 每半分钟刷新一次不可见框架
    setInterval(function() {
      var frame = document.getElementById('keepAliveFrame');
      if (frame) {
        frame.src = 'keepalive?t=' + Date.now();
      }
    }, 30000);

    // 注册 Service Worker
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', function() {
        navigator.serviceWorker.register('/sw.js').catch(function(err) {
          console.error('SW 注册失败:', err);
        });
      });
    }
  </script>
</body>
</html>

sw.js:

// == sw.js ==
// Service Worker:全站 GET 请求 SWR 缓存 + 预缓存首页

const CACHE_NAME = 'swr-cache-v1';

// 安装时预缓存首页
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.add('/').catch(() => {}))
  );
  self.skipWaiting(); // 立即激活,不等待旧 SW
});

// 激活时接管所有客户端
self.addEventListener('activate', event => {
  event.waitUntil(self.clients.claim());
});

// SWR 策略:拦截所有同源 GET 请求
self.addEventListener('fetch', event => {
  const { request } = event;
  if (request.method !== 'GET') return;

  const url_tmp = new URL(event.request.url);
  
  // 判断 pathname 是否为 /keepalive
  if (url_tmp.pathname === '/keepalive') {
    // 直接返回 "test" 四个字符作为响应
    event.respondWith(
      new Response('test', {
        status: 200,
        headers: { 'Content-Type': 'text/plain' }
      })
    );
    return; // 已处理,不再继续
  }

  // 仅处理同源请求,避免跨域 opaque 响应的复杂性
  const url = new URL(request.url);
  if (url.origin !== self.location.origin) return;

  event.respondWith(
    caches.open(CACHE_NAME).then(cache =>
      cache.match(request).then(cached => {
        // 后台网络请求,用于更新缓存
        const networkFetch = fetch(request).then(response => {
          if (response && response.status === 200) {
            cache.put(request, response.clone());
          }
          return response;
        }).catch(() => {});

        if (cached) {
          // 命中缓存:立即返回,同时后台更新
          event.waitUntil(networkFetch);
          return cached;
        }
        // 未命中:等待网络响应
        return networkFetch;
      })
    )
  );
});
来源: v2ex查看原文