From 53fe281ecc280c9ac08be99ca64605279d54feb0 Mon Sep 17 00:00:00 2001 From: yangbear Date: Tue, 14 Jul 2026 02:03:44 +0800 Subject: [PATCH] Improve WebGL asset caching --- Build/ServiceWorker.js | 21 ++++++++++++++++++++ Build/index.html | 7 +++++++ Build/server.js | 45 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/Build/ServiceWorker.js b/Build/ServiceWorker.js index 4f26862..cc3c0f7 100644 --- a/Build/ServiceWorker.js +++ b/Build/ServiceWorker.js @@ -15,11 +15,32 @@ self.addEventListener('install', function (e) { const cache = await caches.open(cacheName); console.log('[Service Worker] Caching all: app shell and content'); await cache.addAll(contentToCache); + await self.skipWaiting(); + })()); +}); + +self.addEventListener('activate', function (e) { + e.waitUntil((async function () { + const keys = await caches.keys(); + await Promise.all(keys.map(function (key) { + return key === cacheName ? Promise.resolve() : caches.delete(key); + })); + await self.clients.claim(); })()); }); self.addEventListener('fetch', function (e) { + if (e.request.method !== 'GET') { + return; + } + e.respondWith((async function () { + const url = new URL(e.request.url); + const isBuildAsset = url.pathname.includes('/Build/') || url.pathname.includes('/StreamingAssets/'); + if (!isBuildAsset && !url.pathname.endsWith('/TemplateData/style.css')) { + return fetch(e.request); + } + let response = await caches.match(e.request); console.log(`[Service Worker] Fetching resource: ${e.request.url}`); if (response) { return response; } diff --git a/Build/index.html b/Build/index.html index 1625cbd..5a580b2 100644 --- a/Build/index.html +++ b/Build/index.html @@ -69,6 +69,13 @@ showBanner: unityShowBanner, }; + config.cacheControl = function (url) { + if (url === config.dataUrl || url === config.frameworkUrl || url === config.codeUrl) { + return "immutable"; + } + return "must-revalidate"; + }; + // By default Unity keeps WebGL canvas render target size matched with // the DOM size of the canvas element (scaled by window.devicePixelRatio) // Set this to false if you want to decouple this synchronization from diff --git a/Build/server.js b/Build/server.js index 3d8962b..b48702d 100644 --- a/Build/server.js +++ b/Build/server.js @@ -36,8 +36,11 @@ function originalCompressedPath(fp) { } function headersForFile(fp, size, encoding) { + const originalPath = originalCompressedPath(fp); + const fileName = path.basename(originalPath); + const isHashedBuildAsset = /\/Build\/[a-f0-9]{32}\./.test(fp); const headers = { - 'Content-Type': extType(originalCompressedPath(fp)), + 'Content-Type': extType(originalPath), 'Content-Length': size, 'X-Content-Type-Options': 'nosniff', 'Accept-Ranges': 'bytes', @@ -45,11 +48,15 @@ function headersForFile(fp, size, encoding) { if (encoding != null) { headers['Content-Encoding'] = encoding; - headers['Cache-Control'] = 'public, max-age=604800'; + headers['Cache-Control'] = isHashedBuildAsset + ? 'public, max-age=31536000, immutable' + : 'public, max-age=604800'; } else if (/\.(mp4|m4v|webm|ogg)$/i.test(fp)) { - headers['Cache-Control'] = 'public, max-age=604800'; - } else if (fp.endsWith('.loader.js')) { + headers['Cache-Control'] = 'public, max-age=31536000, immutable'; + } else if (fileName === 'ServiceWorker.js' || fileName === 'index.html' || fileName === 'manifest.webmanifest') { headers['Cache-Control'] = 'no-cache'; + } else if (isHashedBuildAsset) { + headers['Cache-Control'] = 'public, max-age=31536000, immutable'; } else { headers['Cache-Control'] = 'public, max-age=3600'; } @@ -57,6 +64,24 @@ function headersForFile(fp, size, encoding) { return headers; } +function addValidators(headers, stat) { + headers['ETag'] = `"${stat.size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`; + headers['Last-Modified'] = stat.mtime.toUTCString(); + return headers; +} + +function isFresh(req, headers) { + const ifNoneMatch = req.headers['if-none-match']; + if (ifNoneMatch && ifNoneMatch === headers['ETag']) return true; + + const ifModifiedSince = req.headers['if-modified-since']; + if (ifModifiedSince && headers['Last-Modified']) { + return new Date(ifModifiedSince).getTime() >= new Date(headers['Last-Modified']).getTime(); + } + + return false; +} + function detectUnityEncoding(fp, callback) { if (fp.endsWith('.gz')) { callback(null, 'gzip'); @@ -242,14 +267,22 @@ function serveStatic(req, res) { return; } - const headers = headersForFile(fp, end - start + 1, encoding); + const headers = addValidators(headersForFile(fp, end - start + 1, encoding), stat); headers['Content-Range'] = `bytes ${start}-${end}/${stat.size}`; res.writeHead(206, headers); fs.createReadStream(fp, { start, end }).pipe(res); return; } - res.writeHead(200, headersForFile(fp, stat.size, encoding)); + const headers = addValidators(headersForFile(fp, stat.size, encoding), stat); + if (isFresh(req, headers)) { + delete headers['Content-Length']; + res.writeHead(304, headers); + res.end(); + return; + } + + res.writeHead(200, headers); fs.createReadStream(fp).pipe(res); }); });