From 9d8e2a933af22544896e4f6ca9c5fd6ae9f1e56f Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 29 Sep 2026 18:29:21 +1000 Subject: [PATCH] perf(registry): load related skills in the browser, not in SSR No-JS crawlers render unique Skill pages in bursts, and each SSR paid about five D1 reads for /api/skill-related. The 2026-09-29 overload was one such burst. Related skills and commit history now load client-side when either section nears the viewport, with skeletons and a scoped retry. --- .../registry/app/components/SkillDetail.vue | 196 ++++++++++++++++-- ...ll-detail-duplicate-canonical-path.test.ts | 8 - test/unit/skill-detail-howto-schema.test.ts | 8 - test/unit/skill-detail-missing-search.test.ts | 9 - 4 files changed, 173 insertions(+), 48 deletions(-) diff --git a/layers/registry/app/components/SkillDetail.vue b/layers/registry/app/components/SkillDetail.vue index 58c58cda..9feb94e3 100644 --- a/layers/registry/app/components/SkillDetail.vue +++ b/layers/registry/app/components/SkillDetail.vue @@ -226,16 +226,57 @@ const skillFetch = useFetch( } | null }>> -const relatedFetch = useFetch( - () => `/api/skill-related/${slug.value}`, - { watch: [slug], immediate: true }, -) as ReturnType> +} + +// Related skills and commit history load in the browser, once either section +// nears the viewport. Crawlers that run no JavaScript render thousands of +// unique skill pages in bursts, and each SSR paid five D1 reads for this +// endpoint (2026-09-29 overload). Browsers and rendering crawlers still get it. +const { + data: relatedData, + error: relatedError, + execute: loadRelated, + clear: clearRelated, +} = useLazyFetch( + () => `/api/skill-related/${slug.value}`, + { server: false, immediate: false, watch: false }, +) +const relatedLoading = computed(() => !relatedData.value && !relatedError.value) + +const historySlot = useTemplateRef('historySlot') +const relatedSlot = useTemplateRef('relatedSlot') +const relatedSlotVisible = ref(false) +const requestedRelatedSlug = ref(null) + +useIntersectionObserver( + () => [historySlot.value, relatedSlot.value], + (entries) => { + relatedSlotVisible.value = entries.some(entry => entry.isIntersecting) + }, + { rootMargin: '1200px 0px' }, +) + +watch([relatedSlotVisible, slug], ([visible, key]) => { + if (requestedRelatedSlug.value !== null && requestedRelatedSlug.value !== key) { + requestedRelatedSlug.value = null + clearRelated() + } + if (!visible || requestedRelatedSlug.value === key) + return + requestedRelatedSlug.value = key + void loadRelated() +}, { flush: 'post' }) + +function retryRelated(): void { + clearRelated() + void loadRelated() +} const liveSkillFetch = useAsyncData( () => `skill-live:${slug.value}`, @@ -249,10 +290,9 @@ const liveSkillFetch = useAsyncData( // Keep complete SSR for search and link previews. Client navigation renders // the loading state immediately while these independent requests run together. if (import.meta.server) - await Promise.all([skillFetch, relatedFetch, liveSkillFetch]) + await Promise.all([skillFetch, liveSkillFetch]) const { data, status, error, refresh } = skillFetch -const { data: relatedData } = relatedFetch const { data: liveSkill } = liveSkillFetch const isMissingSkill = computed(() => error.value?.statusCode === 404) @@ -1907,8 +1947,9 @@ useHead(computed(() => ({ />

({ History

    +
  1. + +
    + + +
    +
  2. +
+ +

+ No recent commits found for this file. +

+
  1. ({ - diff --git a/test/unit/skill-detail-duplicate-canonical-path.test.ts b/test/unit/skill-detail-duplicate-canonical-path.test.ts index 736ec6c5..8d3283f2 100644 --- a/test/unit/skill-detail-duplicate-canonical-path.test.ts +++ b/test/unit/skill-detail-duplicate-canonical-path.test.ts @@ -18,14 +18,6 @@ mockNuxtImport('useFetch', () => (url: unknown) => { if (typeof key === 'string' && key.startsWith('/api/skills/')) return { data: ref(skillPayload()), status: ref('success'), error: ref(null), refresh: vi.fn() } - if (key === `/api/skill-related/${route.params.owner}/${route.params.repo}/${route.params.name}`) { - return { - data: ref({ commits: [], relatedRepoSkills: [], relatedOwnerSkills: [], coOccurrenceSkills: [], semanticSiblings: [] }), - status: ref('success'), - error: ref(null), - refresh: vi.fn(), - } - } return { data: ref(null), status: ref('success'), error: ref(null), refresh: vi.fn() } }) diff --git a/test/unit/skill-detail-howto-schema.test.ts b/test/unit/skill-detail-howto-schema.test.ts index 961b4f69..18bb6726 100644 --- a/test/unit/skill-detail-howto-schema.test.ts +++ b/test/unit/skill-detail-howto-schema.test.ts @@ -33,14 +33,6 @@ mockNuxtImport('useFetch', () => (url: unknown) => { if (typeof key === 'string' && key.startsWith('/api/skills/')) return { data: ref(payload), status: ref('success'), error: ref(null), refresh: vi.fn() } - if (typeof key === 'string' && key.startsWith('/api/skill-related/')) { - return { - data: ref({ commits: [], relatedRepoSkills: [], relatedOwnerSkills: [], coOccurrenceSkills: [], semanticSiblings: [] }), - status: ref('success'), - error: ref(null), - refresh: vi.fn(), - } - } return { data: ref(null), status: ref('success'), error: ref(null), refresh: vi.fn() } }) diff --git a/test/unit/skill-detail-missing-search.test.ts b/test/unit/skill-detail-missing-search.test.ts index 4e6b1ffa..9cf25bee 100644 --- a/test/unit/skill-detail-missing-search.test.ts +++ b/test/unit/skill-detail-missing-search.test.ts @@ -57,15 +57,6 @@ mockNuxtImport('useFetch', () => (url: unknown) => { } } - if (typeof key === 'string' && key.startsWith('/api/skill-related/')) { - return { - data: ref({ commits: [], relatedRepoSkills: [], relatedOwnerSkills: [], coOccurrenceSkills: [], semanticSiblings: [] }), - status: ref('success'), - error: ref(null), - refresh: vi.fn(), - } - } - return { data: ref(null), status: ref('success'), error: ref(null), refresh: vi.fn() } })