From b766825a64812d885c87f46ab49345d8d1b687b3 Mon Sep 17 00:00:00 2001 From: corsacca Date: Wed, 20 May 2026 16:39:58 +0100 Subject: [PATCH] optimize caching --- app/layouts/default.vue | 18 ++++++++++++++++++ app/plugins/statinator.client.ts | 11 ++++++++++- nuxt.config.ts | 8 ++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/layouts/default.vue b/app/layouts/default.vue index 257d59e5..796b8117 100644 --- a/app/layouts/default.vue +++ b/app/layouts/default.vue @@ -5,6 +5,23 @@ // routes — admin pages declare their own typography in admin.css. const { locale, locales } = useI18n() +const { public: pub } = useRuntimeConfig() + +// Warm DNS+TCP+TLS for the cross-origin services these pages hit: the prayer +// data API (people-group components) and the idle-loaded analytics + feedback +// widget scripts. Deriving from runtime config keeps these correct per env. +const preconnectOrigins = computed(() => { + const origins = new Set() + for (const url of [pub.prayBaseUrl, pub.statinatorUrl, pub.feedbackApiBase]) { + if (!url) continue + try { + origins.add(new URL(String(url)).origin) + } catch { + // ignore malformed/empty url + } + } + return [...origins] +}) // Direction attribute per locale (Arabic = rtl) const currentLocale = computed(() => @@ -21,6 +38,7 @@ useHead(() => ({ dir: dir.value }, link: [ + ...preconnectOrigins.value.map(origin => ({ rel: 'preconnect', href: origin })), // Preload the heading font (Bebas Neue) — it renders the above-the-fold // h1/h2 (the LCP text), so fetching it early avoids a swap flash. { rel: 'preload', as: 'font', type: 'font/woff2', href: '/assets/fonts/BebasNeue/BebasNeue-Regular.woff2', crossorigin: '' }, diff --git a/app/plugins/statinator.client.ts b/app/plugins/statinator.client.ts index de0338f8..219b59e9 100644 --- a/app/plugins/statinator.client.ts +++ b/app/plugins/statinator.client.ts @@ -66,7 +66,16 @@ export default defineNuxtPlugin(() => { injectScript().then(() => window.goStats?.pageview()).catch(() => {}) } - if (!isExcluded(useRoute().path)) firePageview() + // Defer the initial pageview's script injection until the browser is idle so + // the analytics bundle stays off the initial load critical path. SPA route + // changes (below) still fire immediately — they happen after load. + if (!isExcluded(useRoute().path)) { + if ('requestIdleCallback' in window) { + requestIdleCallback(() => firePageview(), { timeout: 3000 }) + } else { + setTimeout(firePageview, 2000) + } + } const router = useRouter() router.afterEach((to, from) => { diff --git a/nuxt.config.ts b/nuxt.config.ts index ff811d9d..244cb566 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -109,6 +109,11 @@ export default defineNuxtConfig({ }, routeRules: { + // Content-hashed build assets never change under the same URL — cache forever. + '/_nuxt/**': { headers: { 'cache-control': 'public, max-age=31536000, immutable' } }, + // Fonts/images use stable filenames, so avoid `immutable`: a 7-day cache + // still lets an overwritten asset propagate within a week. + '/assets/**': { headers: { 'cache-control': 'public, max-age=604800' } }, '/': { prerender: true }, '/adopt': { prerender: true }, '/pray': { prerender: true }, @@ -132,6 +137,9 @@ export default defineNuxtConfig({ // server crashes on boot. The bun preset builds the output for Bun's own // resolver. Run it with `bun run ./.output/server/index.mjs`. preset: 'bun', + // Precompress public assets (JS/CSS/HTML/SVG/JSON) to .br and .gz at build + // time; the server serves the compressed variant when the client supports it. + compressPublicAssets: { brotli: true, gzip: true }, // Dev-only: use in-memory cache for the payload mount. The default fs // driver can't represent both a leaf route ("/fr") and children of that // route ("/fr/adopt") simultaneously — the first visit writes a file at