mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
perf(pwa): reduce precache asset fanout
Limit PWA precache to app shell assets and add bounded runtime caching for scoped deployments.
This commit is contained in:
+4
-1
@@ -33,7 +33,10 @@
|
|||||||
"stream-browserify",
|
"stream-browserify",
|
||||||
// The PWA service worker entry is injected by vite-plugin-pwa, which Knip does not trace here.
|
// The PWA service worker entry is injected by vite-plugin-pwa, which Knip does not trace here.
|
||||||
"workbox-core",
|
"workbox-core",
|
||||||
"workbox-precaching"
|
"workbox-expiration",
|
||||||
|
"workbox-precaching",
|
||||||
|
"workbox-routing",
|
||||||
|
"workbox-strategies"
|
||||||
],
|
],
|
||||||
"ignoreIssues": {
|
"ignoreIssues": {
|
||||||
// This import is intentionally satisfied transitively through bitsocial-react-hooks.
|
// This import is intentionally satisfied transitively through bitsocial-react-hooks.
|
||||||
|
|||||||
@@ -44,7 +44,10 @@
|
|||||||
"tcp-port-used": "1.0.2",
|
"tcp-port-used": "1.0.2",
|
||||||
"typescript": "6.0.2",
|
"typescript": "6.0.2",
|
||||||
"workbox-core": "7.4.0",
|
"workbox-core": "7.4.0",
|
||||||
|
"workbox-expiration": "7.4.0",
|
||||||
"workbox-precaching": "7.4.0",
|
"workbox-precaching": "7.4.0",
|
||||||
|
"workbox-routing": "7.4.0",
|
||||||
|
"workbox-strategies": "7.4.0",
|
||||||
"zustand": "4.4.3"
|
"zustand": "4.4.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,26 +2,51 @@
|
|||||||
/* eslint-disable no-restricted-globals */
|
/* eslint-disable no-restricted-globals */
|
||||||
|
|
||||||
import { clientsClaim } from 'workbox-core';
|
import { clientsClaim } from 'workbox-core';
|
||||||
|
import { ExpirationPlugin } from 'workbox-expiration';
|
||||||
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
|
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
|
||||||
import { registerRoute } from 'workbox-routing';
|
import { registerRoute } from 'workbox-routing';
|
||||||
import { NetworkFirst } from 'workbox-strategies';
|
import { NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
|
||||||
|
|
||||||
declare const self: ServiceWorkerGlobalScope;
|
declare const self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
const precacheEntries = self.__WB_MANIFEST.filter((entry) => (typeof entry === 'string' ? entry !== 'index.html' : entry.url !== 'index.html'));
|
const precacheEntries = self.__WB_MANIFEST.filter((entry) => (typeof entry === 'string' ? entry !== 'index.html' : entry.url !== 'index.html'));
|
||||||
|
const runtimeAssetDestinations = new Set<RequestDestination>(['font', 'image', 'manifest', 'script', 'style']);
|
||||||
|
const scopeRoot = self.registration.scope.endsWith('/') ? self.registration.scope : `${self.registration.scope}/`;
|
||||||
|
const scopePath = (path: string) => new URL(path, scopeRoot).pathname;
|
||||||
|
const apiPath = scopePath('api');
|
||||||
|
const apiPathPrefix = scopePath('api/');
|
||||||
|
const internalPathPrefix = scopePath('_(');
|
||||||
|
const runtimeAssetPathPrefixes = ['assets/', 'translations/'].map(scopePath);
|
||||||
|
const isApiPath = (pathname: string) => pathname === apiPath || pathname.startsWith(apiPathPrefix);
|
||||||
|
|
||||||
// Precache revisioned assets, but let navigations fetch fresh HTML first.
|
// Precache revisioned assets, but let navigations fetch fresh HTML first.
|
||||||
cleanupOutdatedCaches();
|
cleanupOutdatedCaches();
|
||||||
precacheAndRoute(precacheEntries);
|
precacheAndRoute(precacheEntries);
|
||||||
|
|
||||||
registerRoute(
|
registerRoute(
|
||||||
({ request, url }) => request.mode === 'navigate' && !url.pathname.startsWith('/api') && !/^\/_\(.*\)/.test(url.pathname),
|
({ request, url }) => request.mode === 'navigate' && !isApiPath(url.pathname) && !url.pathname.startsWith(internalPathPrefix),
|
||||||
new NetworkFirst({
|
new NetworkFirst({
|
||||||
cacheName: 'html-cache',
|
cacheName: 'html-cache',
|
||||||
networkTimeoutSeconds: 3,
|
networkTimeoutSeconds: 3,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
registerRoute(
|
||||||
|
({ request, url }) =>
|
||||||
|
url.origin === self.location.origin &&
|
||||||
|
(runtimeAssetDestinations.has(request.destination) || runtimeAssetPathPrefixes.some((prefix) => url.pathname.startsWith(prefix))),
|
||||||
|
new StaleWhileRevalidate({
|
||||||
|
cacheName: 'runtime-static-assets',
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({
|
||||||
|
maxEntries: 200,
|
||||||
|
maxAgeSeconds: 60 * 60 * 24 * 30,
|
||||||
|
purgeOnQuotaError: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Standard SW lifecycle methods
|
// Standard SW lifecycle methods
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
clientsClaim();
|
clientsClaim();
|
||||||
|
|||||||
+75
-6
@@ -6,6 +6,74 @@ import { VitePWA } from 'vite-plugin-pwa';
|
|||||||
|
|
||||||
const { version: packageVersion } = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
const { version: packageVersion } = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
||||||
const appVersion = `${process.env.VITE_APP_VERSION || packageVersion}`.trim() || packageVersion;
|
const appVersion = `${process.env.VITE_APP_VERSION || packageVersion}`.trim() || packageVersion;
|
||||||
|
const publicBase = process.env.PUBLIC_URL || '/';
|
||||||
|
const buildOutDir = 'build';
|
||||||
|
const basePathPrefix = (() => {
|
||||||
|
const pathname = new URL(publicBase, 'https://example.invalid/').pathname;
|
||||||
|
return pathname === '/' ? '' : pathname.replace(/^\/+|\/+$/g, '');
|
||||||
|
})();
|
||||||
|
const neverPrecacheUrls = new Set(['index.html', 'version.json']);
|
||||||
|
const vitePwaManagedAssetUrls = new Set([
|
||||||
|
'manifest.webmanifest',
|
||||||
|
'favicon.ico',
|
||||||
|
'favicon2.ico',
|
||||||
|
'robots.txt',
|
||||||
|
'apple-touch-icon.png',
|
||||||
|
'manifest-icon-192x192.png',
|
||||||
|
'manifest-icon-512x512.png',
|
||||||
|
]);
|
||||||
|
const baselineAppShellUrls = new Set([
|
||||||
|
'registerSW.js',
|
||||||
|
'manifest.json',
|
||||||
|
'manifest.webmanifest',
|
||||||
|
'favicon.ico',
|
||||||
|
'favicon2.ico',
|
||||||
|
'robots.txt',
|
||||||
|
'apple-touch-icon.png',
|
||||||
|
'manifest-icon-192x192.png',
|
||||||
|
'manifest-icon-512x512.png',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function normalizePrecacheUrl(url) {
|
||||||
|
const normalizedUrl = url.split('?')[0].replace(/^[./]+/, '');
|
||||||
|
|
||||||
|
if (basePathPrefix && normalizedUrl.startsWith(`${basePathPrefix}/`)) {
|
||||||
|
return normalizedUrl.slice(basePathPrefix.length + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectIndexAssetUrls() {
|
||||||
|
const indexHtml = readFileSync(new URL(`./${buildOutDir}/index.html`, import.meta.url), 'utf8');
|
||||||
|
const urls = new Set(baselineAppShellUrls);
|
||||||
|
const assetAttributePattern = /\b(?:href|src)=["'](?:\.\/|\/)?([^"']+\.(?:css|ico|js|json|png|webmanifest))(?:\?[^"']*)?["']/g;
|
||||||
|
|
||||||
|
for (const match of indexHtml.matchAll(assetAttributePattern)) {
|
||||||
|
urls.add(normalizePrecacheUrl(match[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
function keepAppShellPrecacheOnly(manifestEntries) {
|
||||||
|
const appShellUrls = collectIndexAssetUrls();
|
||||||
|
const manifest = manifestEntries.filter((entry) => {
|
||||||
|
const normalizedUrl = normalizePrecacheUrl(entry.url);
|
||||||
|
|
||||||
|
if (neverPrecacheUrls.has(normalizedUrl)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vitePwaManagedAssetUrls.has(normalizedUrl)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return appShellUrls.has(normalizedUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { manifest };
|
||||||
|
}
|
||||||
|
|
||||||
function appVersionMetadataPlugin() {
|
function appVersionMetadataPlugin() {
|
||||||
const payload = `${JSON.stringify({ version: appVersion })}\n`;
|
const payload = `${JSON.stringify({ version: appVersion })}\n`;
|
||||||
@@ -85,7 +153,8 @@ export default defineConfig({
|
|||||||
strategies: 'injectManifest',
|
strategies: 'injectManifest',
|
||||||
injectManifest: {
|
injectManifest: {
|
||||||
maximumFileSizeToCacheInBytes: 20000000,
|
maximumFileSizeToCacheInBytes: 20000000,
|
||||||
globIgnores: ['**/version.json'],
|
globPatterns: ['**/*.{css,html,ico,js,json,png,webmanifest}'],
|
||||||
|
manifestTransforms: [keepAppShellPrecacheOnly],
|
||||||
},
|
},
|
||||||
srcDir: 'src',
|
srcDir: 'src',
|
||||||
filename: 'sw.ts',
|
filename: 'sw.ts',
|
||||||
@@ -103,17 +172,17 @@ export default defineConfig({
|
|||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: '/android-chrome-192x192.png',
|
src: 'manifest-icon-192x192.png',
|
||||||
sizes: '192x192',
|
sizes: '192x192',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/android-chrome-512x512.png',
|
src: 'manifest-icon-512x512.png',
|
||||||
sizes: '512x512',
|
sizes: '512x512',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/android-chrome-512x512.png',
|
src: 'manifest-icon-512x512.png',
|
||||||
sizes: '512x512',
|
sizes: '512x512',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
purpose: 'any maskable',
|
purpose: 'any maskable',
|
||||||
@@ -216,7 +285,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
// Use 'build' to match what electron/main.js expects (../build/index.html)
|
// Use 'build' to match what electron/main.js expects (../build/index.html)
|
||||||
outDir: 'build',
|
outDir: buildOutDir,
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
sourcemap: process.env.GENERATE_SOURCEMAP === 'true',
|
sourcemap: process.env.GENERATE_SOURCEMAP === 'true',
|
||||||
target: process.env.ELECTRON ? 'electron-renderer' : 'esnext',
|
target: process.env.ELECTRON ? 'electron-renderer' : 'esnext',
|
||||||
@@ -248,7 +317,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
base: process.env.PUBLIC_URL || '/',
|
base: publicBase,
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ['ethers', 'assert', 'buffer', 'process', 'util', 'stream-browserify', 'isomorphic-fetch', 'workbox-core', 'workbox-precaching'],
|
include: ['ethers', 'assert', 'buffer', 'process', 'util', 'stream-browserify', 'isomorphic-fetch', 'workbox-core', 'workbox-precaching'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -90,7 +90,10 @@ __metadata:
|
|||||||
wait-on: "npm:9.0.3"
|
wait-on: "npm:9.0.3"
|
||||||
workbox-build: "npm:7.4.0"
|
workbox-build: "npm:7.4.0"
|
||||||
workbox-core: "npm:7.4.0"
|
workbox-core: "npm:7.4.0"
|
||||||
|
workbox-expiration: "npm:7.4.0"
|
||||||
workbox-precaching: "npm:7.4.0"
|
workbox-precaching: "npm:7.4.0"
|
||||||
|
workbox-routing: "npm:7.4.0"
|
||||||
|
workbox-strategies: "npm:7.4.0"
|
||||||
workbox-window: "npm:7.4.0"
|
workbox-window: "npm:7.4.0"
|
||||||
zustand: "npm:4.4.3"
|
zustand: "npm:4.4.3"
|
||||||
dependenciesMeta:
|
dependenciesMeta:
|
||||||
|
|||||||
Reference in New Issue
Block a user