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:
parent
0ebc109a88
commit
552647b2c3
21 changed files with 1264 additions and 1475 deletions
|
|
@ -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"],
|
||||
|
|
|
|||
2516
js/cf-webapp/package-lock.json
generated
2516
js/cf-webapp/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
5
js/cf-webapp/src/app/(dashboard)/dashboard/layout.tsx
Normal file
5
js/cf-webapp/src/app/(dashboard)/dashboard/layout.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { ReactNode } from "react"
|
||||
|
||||
export default function DashboardInnerLayout({ children }: { children: ReactNode }) {
|
||||
return <main>{children}</main>
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"> & {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 */}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue