perf: webapp CWV optimization — layout restructure + render-blocking fixes (#2598)

## Summary
- Remove 6 render-blocking font `@import` URLs from onboarding CSS,
replace with `next/font` CSS variables
- Delete dead `tailwind.css` (not imported anywhere)
- Scope Crisp chat widget to dashboard routes only (was loading on every
page)
- Add `preconnect`/`dns-prefetch` hints for Intercom
- Add `serverExternalPackages` for `@anthropic-ai/sdk` and `sharp`
- **Restructure layout hierarchy**: move `ViewModeProvider`,
`PrivacyModeProvider`, and sidebar shell out of root layout into
`(dashboard)` group — non-dashboard pages (auth, onboarding,
observability, trace) are now pure server-rendered
- Move `/dashboard` route into `(dashboard)` group, remove duplicate
onboarding guard
- Update semver-compatible dependencies (~30 patch/minor bumps)

## Test plan
- [ ] `npm run build` passes (32 routes, 0 errors)
- [ ] Dashboard pages show sidebar, breadcrumb, org switcher, privacy
toggle
- [ ] `/dashboard` still accessible and shows sidebar
- [ ] Auth/onboarding pages render without sidebar
- [ ] Observability pages render with ObservabilityNav (no sidebar)
- [ ] `/` redirects to `/apikeys`
- [ ] Fonts render correctly on onboarding pages
- [ ] Crisp chat loads on dashboard pages only
- [ ] Intercom loads on all pages
This commit is contained in:
Kevin Turcios 2026-04-10 08:40:59 -05:00 committed by GitHub
parent 0ebc109a88
commit 552647b2c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1264 additions and 1475 deletions

View file

@ -46,6 +46,7 @@ const nextConfig = {
'module': { browser: './src/lib/empty-shim.js' },
},
},
serverExternalPackages: ["@anthropic-ai/sdk", "sharp"],
experimental: {
serverActions: {
allowedOrigins: ["app.codeflash.ai", "localhost:3000"],

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,3 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600");
@import url("https://api.fontshare.com/css?f[]=inter@300,400,600&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Source Code Pro:wght@400");
@import url("https://api.fontshare.com/css?f[]=source code pro@400&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Sora:wght@400;600");
@import url("https://api.fontshare.com/css?f[]=sora@400,600&display=swap");
p,
ol,
ul {
@ -26,33 +19,13 @@ input {
}
.font-inter {
font-family: Inter;
font-family: var(--font-sans), "Inter", sans-serif;
}
.font-source_code_pro {
font-family: "Source Code Pro";
font-family: var(--font-source-code-pro), "Source Code Pro", monospace;
}
.font-sora {
font-family: Sora;
}
p,
ol,
ul {
margin: 0px;
}
button {
padding: 0px;
}
ol,
ul {
padding-inline-start: 1.5em;
}
input {
background-image: none;
background-color: transparent;
font-family: var(--font-sora), "Sora", sans-serif;
}

View file

@ -0,0 +1,5 @@
import { ReactNode } from "react"
export default function DashboardInnerLayout({ children }: { children: ReactNode }) {
return <main>{children}</main>
}

View file

@ -2,6 +2,10 @@ import { auth0 } from "@/lib/auth0"
import { redirect } from "next/navigation"
import { ReactNode } from "react"
import { hasCompletedOnboarding } from "@codeflash-ai/common"
import Script from "next/script"
import { ViewModeProvider } from "../app/ViewModeContext"
import { PrivacyModeProvider } from "../app/PrivacyModeContext"
import { DashboardShell } from "@/components/dashboard-shell"
export default async function DashboardLayout({ children }: { children: ReactNode }) {
const session = await auth0.getSession()
@ -12,5 +16,20 @@ export default async function DashboardLayout({ children }: { children: ReactNod
redirect("/onboarding")
}
return <>{children}</>
return (
<ViewModeProvider user={session.user}>
<PrivacyModeProvider userId={session.user.sub}>
<DashboardShell user={session.user}>
<Script
id="crisp-chat-script"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `window.$crisp=[];window.CRISP_WEBSITE_ID="3e855999-42a1-4543-accf-afc369edfca0";(function(){d=document;s=d.createElement("script");s.src="https://client.crisp.chat/l.js";s.async=1;d.getElementsByTagName("head")[0].appendChild(s);})();`,
}}
/>
{children}
</DashboardShell>
</PrivacyModeProvider>
</ViewModeProvider>
)
}

View file

@ -5,7 +5,7 @@ import { AccountPayload, createOrUpdateUser, getUserById, prisma } from "@codefl
import { eachDayOfInterval, startOfDay } from "date-fns"
import { GitHubUserSearchResult, Member, UserRole } from "@/lib/types"
import { ActionResponse, createErrorResponse, createSuccessResponse } from "@/lib/action-response"
import { RepositoryWithUsage } from "@/app/dashboard/action"
import { RepositoryWithUsage } from "@/app/(dashboard)/dashboard/action"
import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils"
import { withTiming } from "@/lib/server-action-timing"
import { trackMemberInvited, trackRepositoryConnected } from "@/lib/analytics/tracking"

View file

@ -36,7 +36,7 @@ import {
addRepositoryMemberById,
} from "./action"
import { GitHubUserSearchResult, Member } from "@/lib/types"
import { RepositoryWithUsage } from "@/app/dashboard/action"
import { RepositoryWithUsage } from "@/app/(dashboard)/dashboard/action"
import { useViewMode } from "@/app/app/ViewModeContext"
import { MembersList } from "@/components/members/members-list"
import { UserSearchModal } from "@/components/members/user-search-modal"

View file

@ -16,7 +16,7 @@ import Image from "next/image"
import { Card } from "@/components/ui/card"
import Link from "next/link"
import { useRouter } from "next/navigation"
import type { RepositoryWithUsage } from "@/app/dashboard/action"
import type { RepositoryWithUsage } from "@/app/(dashboard)/dashboard/action"
/** Serialized version for server→client boundary (Dates become ISO strings) */
type SerializedRepository = Omit<RepositoryWithUsage, "created_at" | "last_optimized"> & {

View file

@ -1,6 +1,6 @@
import { GitPullRequest } from "lucide-react"
import { getAccountContext } from "@/lib/server/get-account-context"
import { getAllRepositories } from "@/app/dashboard/action"
import { getAllRepositories } from "@/app/(dashboard)/dashboard/action"
import { RepositoryList } from "./_components/RepositoryList"
export default async function RepositoriesPage() {

View file

@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"
import { auth0 } from "@/lib/auth0"
import { cookies } from "next/headers"
import { getUserOrganizations } from "@/components/dashboard/action"
import { getOptimizationPRs } from "@/app/dashboard/action"
import { getOptimizationPRs } from "@/app/(dashboard)/dashboard/action"
import type { AccountPayload } from "@codeflash-ai/common"
export async function GET(request: NextRequest) {

View file

@ -1,16 +0,0 @@
import { auth0 } from "@/lib/auth0"
import { redirect } from "next/navigation"
import { ReactNode } from "react"
import { hasCompletedOnboarding } from "@codeflash-ai/common"
export default async function DashboardLayout({ children }: { children: ReactNode }) {
const session = await auth0.getSession()
if (!session) return null
const completedOnboarding = await hasCompletedOnboarding(session.user.sub)
if (!completedOnboarding) {
redirect("/onboarding")
}
return <main>{children}</main>
}

View file

@ -1,6 +1,6 @@
import { type JSX } from "react"
import type { Metadata } from "next"
import { Inter as FontSans, JetBrains_Mono } from "next/font/google"
import { Inter as FontSans, JetBrains_Mono, Sora, Source_Code_Pro } from "next/font/google"
import "./globals.css"
import { cn } from "@/lib/utils"
import { ThemeProvider } from "@/components/theme-provider"
@ -11,9 +11,6 @@ import { auth0 } from "@/lib/auth0"
import Script from "next/script"
import { PHProvider } from "./providers"
import PostHogPageView from "./PostHogPageView"
import { ViewModeProvider } from "./app/ViewModeContext"
import { PrivacyModeProvider } from "./app/PrivacyModeContext"
import { ConditionalLayout } from "@/components/conditional-layout"
const fontSans = FontSans({
subsets: ["latin"],
@ -27,6 +24,20 @@ const jetbrainsMono = JetBrains_Mono({
display: "swap",
})
const sora = Sora({
subsets: ["latin"],
weight: ["400", "600"],
variable: "--font-sora",
display: "swap",
})
const sourceCodePro = Source_Code_Pro({
subsets: ["latin"],
weight: ["400"],
variable: "--font-source-code-pro",
display: "swap",
})
export const metadata: Metadata = {
title: "Codeflash",
description: "Optimize the performance of your code.",
@ -80,6 +91,8 @@ export default async function RootLayout({
<html lang="en" suppressHydrationWarning>
<PHProvider>
<head>
<link rel="preconnect" href="https://widget.intercom.io" />
<link rel="dns-prefetch" href="https://widget.intercom.io" />
<Script
id="intercom-script"
strategy="afterInteractive"
@ -87,19 +100,14 @@ export default async function RootLayout({
__html: intercomSnippet,
}}
/>
<Script
id="crisp-chat-script"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `window.$crisp=[];window.CRISP_WEBSITE_ID="3e855999-42a1-4543-accf-afc369edfca0";(function(){d=document;s=d.createElement("script");s.src="https://client.crisp.chat/l.js";s.async=1;d.getElementsByTagName("head")[0].appendChild(s);})();`,
}}
/>
</head>
<body
className={cn(
"min-h-screen bg-background font-sans antialiased",
fontSans.variable,
jetbrainsMono.variable,
sora.variable,
sourceCodePro.variable,
)}
>
<PostHogPageView />
@ -110,11 +118,7 @@ export default async function RootLayout({
enableSystem
disableTransitionOnChange
>
<ViewModeProvider user={session?.user}>
<PrivacyModeProvider userId={session?.user?.sub}>
<ConditionalLayout user={session?.user}>{children}</ConditionalLayout>
</PrivacyModeProvider>
</ViewModeProvider>
{children}
<Toaster />
<SonnerToaster position="top-right" richColors />
</ThemeProvider>

View file

@ -1,17 +1,9 @@
import { ReactNode } from "react"
import Script from "next/script"
import { ObservabilityNav } from "@/components/observability/observability-nav"
export default function ObservabilityLayout({ children }: { children: ReactNode }) {
return (
<div className="min-h-screen bg-background">
<Script
id="hide-crisp-chat"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `(function(){var h=function(){if(window.$crisp){$crisp.push(["do","chat:hide"])}else{setTimeout(h,200)}};h()})();`,
}}
/>
<ObservabilityNav />
<div className="flex-1">{children}</div>
</div>

View file

@ -1,60 +0,0 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600");
@import url("https://api.fontshare.com/css?f[]=inter@300,400,600&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Source Code Pro:wght@400");
@import url("https://api.fontshare.com/css?f[]=source code pro@400&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Sora:wght@400;600");
@import url("https://api.fontshare.com/css?f[]=sora@400,600&display=swap");
@tailwind components;
@tailwind utilities;
@layer components {
.all-\[unset\] {
all: unset;
}
.font-inter {
font-family: "Inter";
}
.font-source_code_pro {
font-family: "Source Code Pro";
}
.font-sora {
font-family: "Sora";
}
}
:root {
--black-300: rgba(182, 175, 175, 1);
--black-500: rgba(11, 10, 10, 1);
--black-700: rgba(80, 73, 73, 1);
--gray-900: rgba(17, 25, 40, 1);
--h-3-font-family: "Inter", Helvetica;
--h-3-font-size: 24px;
--h-3-font-style: normal;
--h-3-font-weight: 600;
--h-3-letter-spacing: -0.14400000572204594px;
--h-3-line-height: 32px;
--sub-h-btn-semib-16-font-family: "Sora", Helvetica;
--sub-h-btn-semib-16-font-size: 16px;
--sub-h-btn-semib-16-font-style: normal;
--sub-h-btn-semib-16-font-weight: 600;
--sub-h-btn-semib-16-letter-spacing: 0px;
--sub-h-btn-semib-16-line-height: 150%;
--subtle-font-family: "Inter", Helvetica;
--subtle-font-size: 14px;
--subtle-font-style: normal;
--subtle-font-weight: 400;
--subtle-letter-spacing: 0px;
--subtle-line-height: 20px;
--text-xs-font-medium-font-family: "Inter", Helvetica;
--text-xs-font-medium-font-size: 12px;
--text-xs-font-medium-font-style: normal;
--text-xs-font-medium-font-weight: 500;
--text-xs-font-medium-letter-spacing: 0px;
--text-xs-font-medium-line-height: 150%;
--white: rgba(255, 255, 255, 1);
--yellow-500: rgba(255, 192, 67, 1);
}

View file

@ -1,6 +1,5 @@
"use client"
import { usePathname } from "next/navigation"
import { useEffect, useState } from "react"
import { Sidebar } from "./dashboard/sidebar"
import { Breadcrumb } from "./dashboard/bread-crumb"
@ -9,28 +8,9 @@ import { TOP_BAR_ANNOUNCEMENT } from "@/config/announcements"
import { cn } from "@/lib/utils"
import type { User } from "@auth0/nextjs-auth0/types"
const HIDDEN_PAGES = ["/onboarding", "/codeflash/auth", "/login", "/codeflash/auth/callback"]
export function ConditionalLayout({
children,
user,
}: {
children: React.ReactNode
user?: User | null
}) {
const pathname = usePathname()
export function DashboardShell({ children, user }: { children: React.ReactNode; user?: User }) {
const [isAnnouncementVisible, setIsAnnouncementVisible] = useState(true)
const shouldHideLayout =
pathname !== null &&
(HIDDEN_PAGES.includes(pathname) ||
pathname.startsWith("/trace/") ||
pathname.startsWith("/observability") ||
pathname.startsWith("/roadmap") ||
pathname.startsWith("/report") ||
pathname.startsWith("/membench") ||
!user)
// Auto-collapse announcement after 4 seconds
useEffect(() => {
if (TOP_BAR_ANNOUNCEMENT.enabled && isAnnouncementVisible) {
@ -42,10 +22,6 @@ export function ConditionalLayout({
}
}, [isAnnouncementVisible])
if (shouldHideLayout) {
return <>{children}</>
}
return (
<div className="h-screen flex flex-col">
{/* Full-width top announcement bar */}

View file

@ -33,7 +33,10 @@ import {
} from "lucide-react"
import { formatDistanceToNow } from "date-fns"
import Image from "next/image"
import type { OptimizationPREvent, OptimizationPRsResponse } from "@/app/dashboard/action"
import type {
OptimizationPREvent,
OptimizationPRsResponse,
} from "@/app/(dashboard)/dashboard/action"
interface OptimizationPRsTableProps {
className?: string

View file

@ -35,7 +35,7 @@ import { usePrivacyMode } from "@/app/app/PrivacyModeContext"
import { Breadcrumb } from "./bread-crumb"
import { SIDEBAR_ANNOUNCEMENT } from "@/config/announcements"
import { Switch } from "@/components/ui/switch"
import { getCurrentUserSubscriptionData } from "@/app/dashboard/action"
import { getCurrentUserSubscriptionData } from "@/app/(dashboard)/dashboard/action"
import { Progress } from "@/components/ui/progress"
import { formatCredits, calculateCreditsPercentage, getProgressBarClassName } from "@/lib/utils"
import { isTeamMemberCheck } from "@/lib/team-members"