通过HTTP Link标头实现页面资源预加载(preload)

本文介绍了HTTP预加载技术,特别是利用HTTP Link响应标头和preloadmanifest文件来优化资源加载。通过定义预加载清单,服务端middleware/filter根据条件决定预加载哪些资源。支持HTTP状态码103(EarlyHints)的Chrome103使得预加载更有效。预加载能减少页面加载时间,尤其在网络延迟高的情况下。然而,是否采用预加载需依据实际测试结果,因为可能增加TTFB。

本文介绍一种采用HTTP Link响应标头的资源预加载方式http-preload,通过预先定义preload manifest文件来描述哪个页面需要预加载哪些资源,并使用Node.js, Tomcat, Nginx等多种服务端的middleware/filter读取preload manifest按照其中的描述规则来实现页面资源预加载。

背景

Chrome 103新增了对HTTP状态码 103 (Early Hints)的支持。在此之前的四五年,HTTP状态码 103只是个传说,因为没有浏览器支持它。现在虽有浏览器支持它,但服务端支持HTTP status 103的框架/库却寥寥无几。

操作

第一步:定义Preload Manifest

{
  "$schema": "https://raw.githubusercontent.com/http-preload/manifest/master/preload-v1.schema.json",
  "manifestVersion": 1,
  "conditions": {
    "supportsModulepreload": "(userAgentData, headers) => userAgentData.brands.some((e)=>e.brand==='Chromium'&&parseInt(e.version)>=66)"
  },
  "resources": {
    "/index.html": [
      {"rel": "preload", "href": "/assets/index.css", "as": "style"},
      {"rel": "dns-prefetch", "href": "https://example.com/"}
    ],
    "/index.html supportsModulepreload": [
      {"rel": "modulepreload", "href": "/src/foobar.js"},
      {"rel": "modulepreload", "href": "/lib/foo.js"},
      {"rel": "modulepreload", "href": "/lib/bar.js"},
      {"rel": "modulepreload", "href": "/src/qux.js"}
    ]
  }
}

其中:

  • "$schema"指的是JSON Schema URL,也可以用相对路径指向离线版的preload-v1.schema.json,用以在IDE(如Visual Studio Code)中提供内容辅助、格式校验。
  • "manifestVersion"的值应与所用json schema对版本的要求相匹配
  • "conditions"中每个key是JavaScript标识符(仅ASCII子集),每个value应为函数表达式或箭头函数。
    • 其中参数userAgentData会是类似于Client Hints User Agent API navigator.userAgentData一样的对象,但不限于安全上下文(https协议)
    • 参数headers会是一个包含所有请求头的对象(在Node.js HTTP/2实现中还包含伪请求头:method, :path, :scheme等)
  • “resources"中每个key是页面文件的URI,可选地加一个空格” "和一个已在"conditions"中定义的条件标识符。

该描述文件的作用为:
当服务端收到URI(不含querystring)为"/index.html"的HTTP请求时(或从"/“转发而来),”/index.html"对应的资源列表将被无条件地列为预加载Link请求头,而"/index.html supportsModulepreload"对应地资源列表是否应该发送到客户端取决于函数调用supportsModulepreload(userAgentData, headers)的返回值是否等效于true

详细说明请参见 manifest

第二步:安装使用Preload Filter / Middleware

preload-middleware支持在Node.js serve-index, express, koa, local-web-server使用http-preload

preload-servlet4-filter preload-servlet-filter分别支持在Java 1.8 + Tomcat 9, Java 11 + Tomcat 10中使用http-preload

还有preload-njs-filter,支持在Nginx+njs环境中使用的部分http-preload特性(需要手动重新改njs源码并编译njs;尚不支持HTTP status 103 Early Hints)

P.S. Nginx生态虽有模块ngx_http_early_hints声称支持103 Early Hints,但它在HTTP/2.0协议中不可用,更要命的是它很久没得到更新,不兼容较新的Nginx 1.22.0。

最好让http-preload与HTTP/2.0配套使用,因为预加载会使得但并发请求增多,使用HTTP/1.1协议容易导致并发连接过多时HTTP请求出现排队现象,从而有违预加载的初衷。

第三步:测试预加载的效果

检出并运行上述preload filter/middleware git仓库并运行示例程序,打开浏览器DevTools,并访问本地https://127.0.0.1/ 你将发现之前ES模块加载的依赖链变平了,页面用的到资源都会预先加载。

通过HTTP Link: 标头的方式来实现预加载理论上比通过HTML <link>标签的方式来得更及时,但也会略微增加每个请求的TTFB(Time to First Byte),因为服务端要多增加一层过滤器,多执行至少2个if判断。因此不能保证采用http-preload会比采用<link>标签效果更好,开发者是否采用,应以实际测验结果为依据。一般来说,在网络延迟较高的环境下采用http-preload,页面整体的加载速度优势会明显一些。

<think>我们正在讨论使用ServiceWorker实现前端静态资源预加载。根据引用[1],ServiceWorker可以用于离线缓存、背景同步和预加载资源。用户特别询问了预加载的最佳实践和示例代码。首先,预加载的主要目的是提前获取并缓存用户可能很快需要的资源,从而加速后续页面加载或操作。在ServiceWorker中,我们通常在安装阶段(precache)和激活阶段处理资源预加载,也可以在空闲时主动发起请求。最佳实践包括:1.在ServiceWorker安装阶段预缓存关键静态资源(核心资源)2.在用户交互后预测下一步可能需要的资源,并在后台预加载(预测性预加载)3.结合CacheAPI进行存储下面是一个逐步的示例:###步骤1:注册ServiceWorker首先,在页面中注册ServiceWorker。```javascript//主线程代码(例如main.js)if('serviceWorker'innavigator){navigator.serviceWorker.register('/sw.js').then(registration=>{console.log('ServiceWorker注册成功:',registration);}).catch(error=>{console.log('ServiceWorker注册失败:',error);});}```###步骤2:编写ServiceWorker脚本(sw.js)在ServiceWorker脚本中,我们将在`install`事件中预缓存关键资源,并在`activate`事件中清理旧缓存。此外,我们还可以监听`fetch`事件来拦截请求,但预加载主要发生在安装阶段和空闲时。####示例:安装阶段预缓存核心资源```javascript//sw.jsconstCACHE_NAME='my-site-cache-v1';constPRECACHE_URLS=['/',//首页HTML'/styles/main.css','/scripts/main.js','/images/logo.png'//添加其他关键静态资源];self.addEventListener('install',event=>{//执行安装步骤:打开缓存并添加资源event.waitUntil(caches.open(CACHE_NAME).then(cache=>{console.log('缓存已打开,开始添加资源');returncache.addAll(PRECACHE_URLS);}));});```####示例:激活阶段清理旧缓存```javascriptself.addEventListener('activate',event=>{constcacheWhitelist=[CACHE_NAME];event.waitUntil(caches.keys().then(cacheNames=>{returnPromise.all(cacheNames.map(cacheName=>{if(cacheWhitelist.indexOf(cacheName)===-1){returncaches.delete(cacheName);}}));}));});```###步骤3:预测性预加载除了在安装阶段预缓存,我们还可以在用户与页面交互后,预测用户下一步可能访问的资源并进行预加载。例如,当用户将鼠悬停在某个链接上时,预加载该链接页面资源。在主线程中(页面脚本):```javascript//假设我们有一个链接,当用户鼠悬停时预加载页面资源constlink=document.querySelector('a.prefetch-link');link.addEventListener('mouseover',()=>{//通过ServiceWorker发送消息,触发预加载if(navigator.serviceWorker&&navigator.serviceWorker.controller){navigator.serviceWorker.controller.postMessage({type:'prefetch',urls:['/next-page.html','/styles/next-page.css','/scripts/next-page.js']});}});```在ServiceWorker中监听消息事件:```javascriptself.addEventListener('message',event=>{if(event.data.type==='prefetch'){event.waitUntil(caches.open(CACHE_NAME).then(cache=>{returnPromise.all(event.data.urls.map(url=>fetch(url).then(response=>{if(response.ok){returncache.put(url,response);}})));}));}});```###步骤4:在ServiceWorker中处理资源请求最后,我们需要在ServiceWorker中拦截网络请求,并返回缓存中的资源(如果存在)。```javascriptself.addEventListener('fetch',event=>{event.respondWith(caches.match(event.request).then(response=>{//如果缓存中有,则返回缓存资源if(response){returnresponse;}//否则,发起网络请求returnfetch(event.request);}));});```###注意事项1.**缓存更新**:当资源更新时,需要更新缓存版本(修改CACHE_NAME)并重新预缓存。2.**存储空间**:预加载资源时要注意不要超过浏览器的存储配额。3.**预测准确性**:预测性预加载应谨慎使用,避免浪费带宽。以上便是使用ServiceWorker实现前端静态资源预加载的最佳实践和示例代码。通过预缓存关键资源,并在用户交互后预测性地加载后续资源,可以显著提升用户体验。§§相关问题§§1.如何更新ServiceWorker缓存的资源?2.ServiceWorker预加载HTTP/2服务器推送有何区别?3.如何监控ServiceWorker的缓存命中率?</think>### Service Worker 静态资源预加载实践指南 #### 一、核心实现原理 Service Worker 通过拦截网络请求实现资源预加载,主要利用两个关键机制: 1. **安装阶段预缓存**:在 `install` 事件中提前缓存关键资源 2. **运行时动态预取**:在 `fetch` 事件中预测并缓存后续可能需要的资源 ```mermaid graph TD A[注册Service Worker] --> B[Install 事件] B --> C[预缓存核心资源] A --> D[Fetch 事件] D --> E{资源是否在缓存} E -->|是| F[返回缓存] E -->|否| G[网络请求并缓存] G --> H[预测加载关联资源] ``` #### 二、最佳实践方案 **1. 安装阶段预缓存(核心资源)** ```javascript // sw.js const CACHE_NAME = 'v1'; const PRE_CACHE_URLS = [ '/styles/main.css', '/scripts/app.js', '/images/logo.svg', '/offline.html' ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(PRE_CACHE_URLS)) .then(() => self.skipWaiting()) ); }); ``` **2. 运行时动态预加载(预测性加载)** ```javascript self.addEventListener('fetch', event => { // 核心资源响应逻辑 event.respondWith( caches.match(event.request) .then(cached => cached || fetch(event.request)) ); // 预加载关联资源 if (event.request.url.endsWith('.html')) { event.waitUntil( caches.open(CACHE_NAME).then(cache => { return fetch('/related-resources.json') .then(res => res.json()) .then(resources => cache.addAll(resources)); }) ); } }); ``` **3. 预加载策略优化** - **关键路径资源**:优先预加载首屏渲染必需的资源(CSS, JS) - **按需预加载**:在用户交互后预测下一步资源(如 hover 时预加载) - **空闲时加载**:使用 `requestIdleCallback` 加载低优先级资源 ```javascript // 主线程触发预加载 document.getElementById('gallery').addEventListener('mouseover', () => { if (navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage({ type: 'prefetch', urls: ['/images/high-res-1.jpg', '/images/high-res-2.jpg'] }); } }); ``` #### 三、完整实现示例 ```javascript // sw.js const CACHE_NAME = 'precache-v2'; const PRECACHE_MAP = { '/': ['/styles/main.css', '/scripts/app.js'], '/product': ['/scripts/product.js', '/templates/product.hbs'] }; self.addEventListener('install', (event) => { const precacheUrls = Object.values(PRECACHE_MAP).flat(); event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll([...new Set(precacheUrls)])) .then(self.skipWaiting) ); }); self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request) .then(cached => cached || fetch(event.request)) ); // 页面路由预加载 if (event.request.mode === 'navigate') { const path = new URL(event.request.url).pathname; if (PRECACHE_MAP[path]) { event.waitUntil( caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE_MAP[path]) ) ); } } }); self.addEventListener('message', (event) => { if (event.data.type === 'prefetch') { caches.open(CACHE_NAME).then(cache => cache.addAll(event.data.urls) ); } }); ``` #### 四、性能优化技巧 1. **缓存策略**:使用 `max-age=31536000` 配合 `immutable` ```nginx location /static { add_header Cache-Control "public, max-age=31536000, immutable"; } ``` 2. **存储管理**:定期清理旧缓存 ```javascript self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(keys => Promise.all( keys.filter(key => key !== CACHE_NAME) .map(key => caches.delete(key)) )) ); }); ``` 3. **网络降级**:优先返回缓存,网络不可用时提供备用内容 ```javascript event.respondWith( caches.match(event.request) .then(cached => cached || fetch(event.request)) .catch(() => caches.match('/offline.html')) ); ``` #### 五、监控与调试 1. **Chrome DevTools**: - Application > Service Workers 查看运行状态 - Cache Storage 检查缓存内容 2. **性能指**: - `caches.has()` 检测缓存命中率 - `performance.mark()` 测量资源加载时间 > **重要提示**:预加载应遵循"渐进增强"原则,避免过度请求导致带宽浪费。建议通过用户行为分析确定高频访问路径,针对性预加载关键资源[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值