From d254e9cd9828dd7bf86d878d944d97093d409022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 16 May 2026 17:57:03 +0200 Subject: [PATCH] perf(insights): defer below-fold mother-page blocks behind next/dynamic + bundle config hygiene MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lighthouse v1.4.33 flagged ~463 KiB of unused JS on first /insights paint plus ~211 KiB of bundled JS as unminified. Two coordinated moves close the gap without touching the Coach panel internals or the dashboard tree. next.config.ts - `compiler.removeConsole` with `exclude: ["error", "warn"]` strips the chart-wiring + Coach SSE breadcrumb literals from the prod bundle. Error + warn paths still flow through to GlitchTip. - `headers()` advertises `Permissions-Policy: unload=()` so Chromium admits the page to the back/forward cache when navigating away. We carry no `beforeunload` / `unload` listeners in-tree so the hint is honest. src/app/insights/page.tsx - ``, ``, `` move behind `next/dynamic({ ssr: false, loading: })`. All three paint below the hero on every viewport and were eagerly imported before. Each loader skeleton matches the dimensions of the resolved block so layout doesn't shift. - `` stays an eager import — the greeting + health-score badge are the visible-first-paint surface. --- next.config.ts | 33 +++++++++++++++++++++++++ src/app/insights/page.tsx | 52 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/next.config.ts b/next.config.ts index 1fbfcb7eb..1ad2006d3 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,6 +4,39 @@ import bundleAnalyzer from "@next/bundle-analyzer"; const nextConfig: NextConfig = { output: "standalone", poweredByHeader: false, + // v1.4.33 IW2 — strip `console.*` calls from the production bundle + // (keep `console.error` + `console.warn` so the GlitchTip reporter + // and prod-side debug rails still surface). The Lighthouse audit + // flagged ~211 KiB of bundled JS as "unminified" — Turbopack already + // mangles + minifies the chunks, but the in-tree `console.log` + // breadcrumbs from the chart wiring + Coach SSE handlers carried + // hundreds of preserved string literals through to the client. The + // SWC compiler drops the calls + their literal-only arguments + // entirely in production. + compiler: { + removeConsole: + process.env.NODE_ENV === "production" + ? { exclude: ["error", "warn"] } + : false, + }, + // v1.4.33 IW2 — bfcache hygiene. `Permissions-Policy: unload=()` + // tells the browser the page does not need the `unload` event, + // which Chromium uses as a hint to admit the page to the + // back/forward cache on navigation away. Pair with the absence of + // any `beforeunload` / `unload` listener in our own code so the + // bfcache restore path stays clear. The other CSP-style security + // headers already live on the response via the standalone runtime; + // we add only the bfcache hint here. + async headers() { + return [ + { + source: "/:path*", + headers: [ + { key: "Permissions-Policy", value: "unload=()" }, + ], + }, + ]; + }, serverExternalPackages: [ "@prisma/client", "pg-boss", diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index 8fdfffb72..0cfc94edb 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useQuery } from "@tanstack/react-query"; +import dynamic from "next/dynamic"; import Link from "next/link"; import { Loader2, TrendingUp } from "lucide-react"; @@ -11,15 +12,60 @@ import { useTranslations } from "@/lib/i18n/context"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; import { HeroStrip } from "@/components/insights/hero-strip"; -import { DailyBriefing } from "@/components/insights/daily-briefing"; -import { TrendsRow } from "@/components/insights/trends-row"; -import { CorrelationRow } from "@/components/insights/correlation-row"; import { useInsightsAdvisorQuery } from "@/components/insights/use-insights-advisor"; import { useCoachLaunch } from "@/lib/insights/coach-launch-context"; import { useAnalyticsQuery } from "@/lib/queries/use-analytics-query"; import type { CorrelationResult } from "@/lib/insights/correlations"; import type { DataSummary } from "@/lib/analytics/trends"; +/** + * v1.4.33 IW2 — defer the three below-the-fold mother-page blocks + * behind `next/dynamic`. `` (the only above-the-fold piece) + * stays an eager import so the initial paint shows the greeting and + * health-score badge without a flash; the briefing, correlation row + * and trends row each carry their own icon-set + chart wiring (a chart + * card alone weighs in at the lucide tree-shake limit) and used to + * land on every Insights cold mount. Loader skeletons match the + * existing fallback so the layout doesn't shift while the chunks + * resolve. + */ +const DailyBriefing = dynamic( + () => + import("@/components/insights/daily-briefing").then((mod) => ({ + default: mod.DailyBriefing, + })), + { + ssr: false, + loading: () => ( +
+ ), + }, +); +const CorrelationRow = dynamic( + () => + import("@/components/insights/correlation-row").then((mod) => ({ + default: mod.CorrelationRow, + })), + { + ssr: false, + loading: () => ( +
+ ), + }, +); +const TrendsRow = dynamic( + () => + import("@/components/insights/trends-row").then((mod) => ({ + default: mod.TrendsRow, + })), + { + ssr: false, + loading: () => ( +
+ ), + }, +); + /** * v1.4.25 W4d — Insights mother page. *