From 5a13f13e8e1afd4edd3346a00d13ad470ac4f26a Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Wed, 10 Jun 2026 01:06:29 +0300 Subject: [PATCH] perf(p): instant OEM nav + trim 30x-bloated payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OEM detail "slow load" was two things, not the query (DB 135ms / API 13-120ms): 1. The parts-panel link opened a NEW TAB → full SPA cold boot every click. Switch to in-app client navigation on plain click (real href kept, so ctrl/cmd/middle-click still opens a new tab). 2. The /p/oem response shipped each article's oeNumbers + compatible lists (up to 200 each × 60 articles) that the UI never renders — 96% of a 370 KB payload. Ship lean articles; aggregates already carry the cross-refs. 60-article code: 370 KB → ~12 KB. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/integrations/p/p-source-db.service.ts | 13 ++++++++++++- apps/api/src/integrations/p/p.types.ts | 14 +++++++++++--- apps/web/src/components/schema/parts-panel.tsx | 17 +++++++++++------ apps/web/src/routes/dashboard/oem.$code.tsx | 2 -- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/apps/api/src/integrations/p/p-source-db.service.ts b/apps/api/src/integrations/p/p-source-db.service.ts index 1992658..7aee5b8 100644 --- a/apps/api/src/integrations/p/p-source-db.service.ts +++ b/apps/api/src/integrations/p/p-source-db.service.ts @@ -240,7 +240,18 @@ export class PSourceDbService implements OnModuleInit, OnModuleDestroy { query, queryNorm, matched: true, - articles, + // Ship lean articles — the per-article oeNumbers/compatible lists were + // only needed above to build the aggregates; sending them too bloated + // heavy responses ~30x (370 KB → ~12 KB for a 60-article code). + articles: articles.map((a) => ({ + id: a.id, + brand: a.brand, + articleNumber: a.articleNumber, + name: a.name, + spareInfo: a.spareInfo, + images: a.images, + eans: a.eans, + })), aftermarketParts, oeCrossReferences, truncated, diff --git a/apps/api/src/integrations/p/p.types.ts b/apps/api/src/integrations/p/p.types.ts index 85b84ff..6b0f6fd 100644 --- a/apps/api/src/integrations/p/p.types.ts +++ b/apps/api/src/integrations/p/p.types.ts @@ -17,7 +17,9 @@ export interface PImage { thumb: string | null; } -/** One P article whose OE number list contains the queried OEM code. */ +/** One P article whose OE number list contains the queried OEM code. Internal + * shape — `oeNumbers`/`compatible` feed the aggregates and are NOT shipped to + * the client (they're large and unused there; see PArticleSummary). */ export interface PArticle { id: string; brand: string; @@ -30,6 +32,11 @@ export interface PArticle { compatible: PCompatible[]; } +/** What the OEM detail page actually renders per matched article — without the + * heavy per-article OE/compat lists (those are deduped into the top-level + * aggregates). Trimming them cut a 60-article payload from ~370 KB to ~12 KB. */ +export type PArticleSummary = Omit; + /** Response of the OEM detail lookup. `matched: false` covers every miss — * feature disabled, code too short, or no P article carries that OE * number — so the UI has a single empty-state path. */ @@ -37,8 +44,9 @@ export interface POemResult { query: string; queryNorm: string; matched: boolean; - /** Distinct articles whose OE list contains the queried code. */ - articles: PArticle[]; + /** Distinct articles whose OE list contains the queried code (lean — per-article + * OE/compat lists are aggregated below, not duplicated here). */ + articles: PArticleSummary[]; /** Deduped buyable aftermarket part numbers across all matched articles * (the matched articles themselves + their compatibility entries). */ aftermarketParts: Array<{ brand: string; articleNumber: string; thumb: string | null }>; diff --git a/apps/web/src/components/schema/parts-panel.tsx b/apps/web/src/components/schema/parts-panel.tsx index c2312de..1a2fabd 100644 --- a/apps/web/src/components/schema/parts-panel.tsx +++ b/apps/web/src/components/schema/parts-panel.tsx @@ -396,14 +396,13 @@ export function PartsPanel({ {part.oemCode && part.oemCode !== "N/A" && matchedOemCodes.has(part.oemCode) ? ( - // Link ONLY codes with a real cross-reference (new tab, - // keeps catalog/schema context). Plain anchor, not router - // Link — fresh load resolves the route, no router context - // needed here. Unmatched codes fall through to plain text. + // Link ONLY codes with a real cross-reference. Plain + // left-click → in-app client navigation (no full SPA + // reload — the new-tab boot was the slow part). Real href + // kept so ctrl/cmd/middle-click still opens a new tab. + // Unmatched codes fall through to plain text. { @@ -414,6 +413,12 @@ export function PartsPanel({ vehicle_id: vehicleId, category_id: categoryId, }); + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return; + e.preventDefault(); + navigate({ + to: "/dashboard/oem/$code", + params: { code: part.oemCode }, + }); }} > {part.oemCode} diff --git a/apps/web/src/routes/dashboard/oem.$code.tsx b/apps/web/src/routes/dashboard/oem.$code.tsx index 0a1affb..72c67a3 100644 --- a/apps/web/src/routes/dashboard/oem.$code.tsx +++ b/apps/web/src/routes/dashboard/oem.$code.tsx @@ -29,8 +29,6 @@ interface PArticle { spareInfo: string | null; images: Array<{ url: string; thumb: string | null }>; eans: string[]; - oeNumbers: OeNumber[]; - compatible: Array<{ brand: string; article: string }>; } interface POemResult { query: string;