shendaowu

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

  •  
  •   shendaowu · 2h 29m ago · 466 views

    效果

    • 我让 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;
          })
        )
      );
    });
    
    3 replies    2026-06-03 08:57:16 +08:00
    xy2401
        1
    xy2401  
       2h 18m ago
    没有看懂 直接 pwa 应用呢
    shendaowu
        2
    shendaowu  
    OP
       2h 15m ago
    @xy2401 之前听说过,没详细了解,粗略了解了一下感觉很复杂就放弃了。
    Tink
        3
    Tink  
    PRO
       2h 3m ago
    有一个自动优选的方法,利用边缘节点,首次打开也能在 50ms 以内
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   5981 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 44ms · UTC 03:00 · PVG 11:00 · LAX 20:00 · JFK 23:00
    ♥ Do have faith in what you're doing.