首页/文章/javascript

使用 Service Worker 让首页秒开

2024-12-24
15506 分钟
...

在大部分场景预加载是页面性能优化的利器,而对于类似首页这种承担用户第一次访问的页面却无法使用预加载,这时候我们可以用 Stale-While-Revalidate 加速页面访问,策略分 3 步

  1. 在收到页面请求时首先检查缓存,如果命中缓存就直接从缓存中返回给用户
  2. 将缓存返回用户的同时,在后台异步发起网络请求,尝试获取资源的最新版本
  3. 获取成功后更新缓存,下次使用

而这一切的幕后功臣便是 Service Worker,作为一个后台代理在网络与缓存之间搭建桥梁,提供了丰富的缓存管理和资源控制能力,从而实现这一高效策略

为了实现这一策略,需要首先了解一下 Service Worker 的核心 API

Service 基础概念

Service Worker 基础概念可以在这里了解

此处为语雀内容卡片,点击链接查看:www.yuque.com/sunluyong/f…

拦截修改 Response 对象

使用 event.respondWith 可以在 fetch 事件中拦截网络请求并提供自定义响应,一旦调用浏览器会等待提供的 Promise 解析,并将其结果作为响应返回给发起请求的代码

self.addEventListener('fetch', event => {
  event.respondWith(
    
  );
});

比如实现拦截特定请求,可以首先尝试从缓存中获取资源,如果缓存命中则返回缓存内容,否则从网络获取资源并缓存

self.addEventListener('fetch', event => {
  
  const url = new URL(event.request.url);
  if (!url.pathname.startsWith('/page/')) return;
  
  event.respondWith(
    caches.match(event.request) 
      .then(cachedResponse => {
        if (cachedResponse) {
          return cachedResponse; 
        }
        
        return fetch(event.request);
      })
  );
});

必须在 fetch 事件监听器内部的第一时间调用 event.respondWith,否则浏览器将继续使用默认的网络请求处理方式

clone Response 对象缓存

在 Service Worker 中处理网络请求和缓存时,经常会遇到需要 clone 响应对象

const responseToCache = networkResponse.clone();

这是由于 Response 对象是一个可读流,而流具有以下特性

  • 单次消费:Streams 在被消费后就会关闭,不能重新读取
  • 节省资源:适合处理大型数据,如视频流、文件下载等

当读取 Response 的 body 返回给浏览器后,Stream 会被读取并关闭,之后无法再次读取用于缓存。通过 clone Response 对象,可以创建一个独立的副本,确保每个副本的 Stream 都可单独消费

fetch(event.request).then(networkResponse => {
  
  const responseToCache = networkResponse.clone();

  
  event.respondWith(networkResponse);

  
  caches.open(CACHE_NAME).then(cache => {
    cache.put(event.request, responseToCache);
  });
});

event.waitUntil 确保异步任务完成

Service Worker 事件都是异步的,浏览器可能在这些异步操作完成之前终止 Service Worker,导致关键任务(如缓存资源或清理旧缓存)无法正确完成

通过调用 event.waitUntil(promise),可以告诉浏览器要“等待”某个 Promise 完成之后,才认为事件处理完成,这确保了浏览器不会在关键异步操作完成之前终止 Service Worker

比如在激活阶段,通常需要清理旧的缓存

self.addEventListener('activate', event => {
  console.log('[Service Worker] Activate Event');
  const cacheWhitelist = ['my-cache-v2'];

  caches.keys().then(cacheNames => {
    return Promise.all(
      cacheNames.map(cacheName => {
        if (!cacheWhitelist.includes(cacheName)) {
          console.log(`[Service Worker] Deleting old cache: ${cacheName}`);
          return caches.delete(cacheName);
        }
      })
    );
  })
});

浏览器可能在异步缓存清理任务完成之前终止激活过程,导致旧缓存可能未被正确删除,使用 event.waitUntil 可以确保所有清理操作完成

self.addEventListener('activate', event => {
  console.log('[Service Worker] Activate Event');
  const cacheWhitelist = ['my-cache-v2'];
  
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (!cacheWhitelist.includes(cacheName)) {
            console.log(`[Service Worker] Deleting old cache: ${cacheName}`);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

Stale-While-Revalidate 实现

1. 创建目录结构

.
├── app
│   └── index.js
├── package.json
└── public
    ├── favicon.ico
    ├── index.html
    └── sw.js

因为 Service Worker 需要服务端配合,为了简单使用 express 演示

npm install --save express

2. 提供 web 服务

修改 app/index.js,public 目录对外服务,为了演示缓存更新效果,添加了一个带有页面版本号的自定义响应头 x-page-version

const express = require('express');
const path = require('path');

const app = express();
const port = 3000;

app.use(express.static(path.join(__dirname, '../public'), {
  setHeaders: (res) => {
    
    res.set('x-page-version', Math.ceil(Date.now() / 5000));
  }
}));


app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

3. Service Worker 实现

首先是最基础的安装、激活,代码量并不大,主要是添加了很多 log 方便观测 Service Worker 执行过程

const CACHE_NAME = 'HOMEPAGE_CACHE_v1'; 


const urlsToCache = [
  '/',
];


self.addEventListener('install', (event) => {
  console.log('[Service Worker] Install Event');
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log('[Service Worker] Caching pre-defined resources');
      return cache.addAll(urlsToCache);
    }).catch((error) => {
      console.error('[Service Worker] Failed to cache resources during install:', error);
    })
  );
});


self.addEventListener('activate', (event) => {
  console.log('[Service Worker] Activate Event');
  const cacheWhitelist = [CACHE_NAME];
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (!cacheWhitelist.includes(cacheName)) {
            console.log(`[Service Worker] Deleting old cache: ${cacheName}`);
            return caches.delete(cacheName);
          }
        })
      );
    }).then(() => self.clients.claim()) 
  );
});

4. 劫持页面请求


self.addEventListener('fetch', (event) => {
  const requestUrl = new URL(event.request.url);
  
  if (!urlsToCache.includes(requestUrl.pathname)) return;

  
  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      if (cachedResponse) {
        
        console.log(`[Service Worker] Serving from cache: ${event.request.url}`);

        
        event.waitUntil(
          fetch(event.request).then((networkResponse) => {
            if (networkResponse && networkResponse.status === 200) {
              return caches.open(CACHE_NAME).then((cache) => {
                
                cache.put(event.request, networkResponse.clone());
                console.log(`[Service Worker] Fetched and cached (background): ${event.request.url}`);
              });
            }
          }).catch((error) => {
            console.error(`[Service Worker] Background fetch failed for: ${event.request.url}`, error);
          })
        );

        return cachedResponse; 
      }

      
      return fetch(event.request).catch((error) => {
        console.error(`[Service Worker] Fetch failed for: ${event.request.url}`, error);
      });
    })
  );
});

这样就基本实现了 Stale-While-Revalidate

5. 注册 Service Worker

在主线程激活 Service Worker

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/sw.js").then(registration => {
    console.log(`Service Worker registered with scope: ${registration.scope}`);
  }).catch(error => {
    console.log(`Service Worker registration failed: ${error}`);
  });
}

更进一步

可以对上面 demo 改进一下,当获取到最新版本页面后和缓存对比,如果发现页面版本已更新,可以给主线程发送通知,让页面重新发请求,获取最新版本的缓存

更新 fetch 事件处理


self.addEventListener('fetch', (event) => {
  const requestUrl = new URL(event.request.url);
  
  if (!urlsToCache.includes(requestUrl.pathname)) return;

  
  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      if (cachedResponse) {
        
        console.log(`[Service Worker] Serving from cache: ${event.request.url}`);

        
        event.waitUntil(
          fetch(event.request).then((networkResponse) => {
            if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
              
              const cachedVersion = cachedResponse.headers.get('x-page-version');
              
              const networkVersion = networkResponse.headers.get('x-page-version');
              console.log(`[Service Worker] Cached Version: ${cachedVersion}`);
              console.log(`[Service Worker] Network Version: ${networkVersion}`);
              
              if (networkVersion !== cachedVersion) {
                return caches.open(CACHE_NAME).then((cache) => {
                  cache.put(event.request, networkResponse.clone());
                  console.log(`[Service Worker] Fetched and cached (background): ${event.request.url}`);

                  
                  return sendMessage({
                    version: networkVersion,
                    action: 'update',
                    url: event.request.url,
                  });
                });
              }
            }
          }).catch((error) => {
            console.error(`[Service Worker] Background fetch failed for: ${event.request.url}`, error);
          })
        );

        return cachedResponse; 
      }

      
      return fetch(event.request).catch((error) => {
        console.error(`[Service Worker] Fetch failed for: ${event.request.url}`, error);
      });
    })
  );
});


function sendMessage(data) {
  return self.clients.matchAll().then((clients) => {
    clients.forEach((client) => {
      client.postMessage(data);
    });
  });
}

更新主线程,添加接收来自 Service Worker 消息事件

navigator.serviceWorker.addEventListener("message", event => {
  console.log('Received a message from Service Worker:', event.data);
  if (event.data.action === "update") {
    if (event.data.url === window.location.href ) {
      console.log('load lasted version');
      location.href = event.data.url;
    }
  }
});

这就是 alibaba.com 秒开的秘籍

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2026-02-24
navigator.sendBeacon全指南
在前端开发中,埋点系统是必不可少的一环。我们经常需要在用户 关闭页面 、 刷新 或 跳转路由 时,向服务器发送最后一条统计数据(比如用户停留时长、页面跳出率)。 但这看似简单的需求,在实现时却危机四伏:请求发不出去?页面跳转卡顿?今天我们就来聊聊这个问题的终极解决方案 —— 。 一、 痛点与传统方案的挣扎 场景还原 当用户点击关闭按钮时,浏览器会触发生命周期事件( 或 )。如果我们直接使用普通的异步 AJAX ( 或 ) 发送请求,浏览...
学习
javascript2025-11-02
理解浏览器事件系统,从用户点击到事件对象的完整旅程
深入理解浏览器事件系统:从用户点击到事件对象的完整旅程 “当我点击页面按钮时,背后发生了什么?为什么回调函数能收到一个包含丰富信息的event对象?今天,让我们一起揭开浏览器事件系统的神秘面纱。” 一个令人困惑的现象 作为前端开发者,我们每天都在写这样的代码: 这段代码如此熟悉,以至于我们很少停下来思考:​ ​这个 对象到底从哪里来?它为什么能知道点击的精确坐标?为什么能识别是哪个元素被点击了?​ ​ 更神奇的是,当我们手动创建事件时:...
学习
javascript2025-10-01
实现大文件上传全流程详解
在日常开发中,大文件上传是个绕不开的坎——动辄几百 MB 甚至 GB 级的文件,直接上传不仅容易超时,还会让用户体验大打折扣。最近我用 Vue+Express 实现了一套完整的大文件上传方案,支持分片上传、断点续传、秒传和手动中。 一、先看效果:我们要实现什么? 先上核心功能清单,确保大家明确目标,知道我们要解决哪些实际问题: 大文件分片上传 :将文件切成固定大小的小片段分批上传,避免单次请求超时 秒传 :服务器已存在完整文件时,直接返...
学习
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-09-15
一张 8K 海报差点把首屏拖垮
你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。 老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。 解决方案:三条技术路线,你全踩了一遍 1\. 最偷懒:原生 一行代码就能跑,浏览器帮你搞定。 🔍 关键决策点 2020 年后现代浏览器全覆盖,IE 全军覆没。 必须写死 ,否则 CLS 会抖成 PPT。 适用场景...
学习
javascript2025-09-10
🚀 Web Worker让你的应用丝滑
🌟 引言 在日常的前端开发中,你是否遇到过这样的困扰: 大数据处理时页面卡死 :处理几万条数据时,页面直接卡成PPT,用户点击毫无反应 复杂计算阻塞UI :图片处理、数据分析等计算密集型任务让整个应用假死 文件上传/下载卡顿 :大文件操作时,其他功能完全无法使用 实时数据处理性能差 :WebSocket接收大量数据时,页面渲染严重滞后 今天分享6个Web Worker的核心技巧,让你的应用告别卡顿,用户体验丝滑如德芙! 💡 核心技巧...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录