perf(insights): defer below-fold mother-page blocks behind next/dynamic + bundle config hygiene

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
- `<DailyBriefing>`, `<CorrelationRow>`, `<TrendsRow>` move behind
  `next/dynamic({ ssr: false, loading: <Skeleton /> })`. 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.
- `<HeroStrip>` stays an eager import — the greeting + health-score
  badge are the visible-first-paint surface.
This commit is contained in:
Marc-André Bombeck committed 2026-05-16 17:57:03 +02:00
1 parent 432ebccdae
commit d254e9cd98
2 files changed
+82 -3

No files matched your search

+33
View File
@@ -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",
+49 -3
View File
@@ -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`. `<HeroStrip>` (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: () => (
<div className="bg-card border-border h-48 animate-pulse rounded-xl border" />
),
},
);
const CorrelationRow = dynamic(
() =>
import("@/components/insights/correlation-row").then((mod) => ({
default: mod.CorrelationRow,
})),
{
ssr: false,
loading: () => (
<div className="bg-card border-border h-32 animate-pulse rounded-xl border" />
),
},
);
const TrendsRow = dynamic(
() =>
import("@/components/insights/trends-row").then((mod) => ({
default: mod.TrendsRow,
})),
{
ssr: false,
loading: () => (
<div className="bg-card border-border h-64 animate-pulse rounded-xl border" />
),
},
);
/**
* v1.4.25 W4d — Insights mother page.
*