From be883aac028e477d42c1fcd937460565385b5497 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Wed, 26 Mar 2025 22:04:52 -0300 Subject: [PATCH] perf: Remove ssrInit from layout and event-types (#20368) * perf: Remove ssrInit from layout and event-types * remove ssg file since not used * refator trpc usage in app router * fix --------- Co-authored-by: hbjORbj --- .../(main-nav)/event-types/page.tsx | 3 - .../(use-page-wrapper)/auth/logout/page.tsx | 3 - apps/web/app/_trpc/HydrateClient.tsx | 8 -- apps/web/app/_trpc/createHydrateClient.tsx | 26 ---- apps/web/app/_trpc/query-client.ts | 37 +++++ apps/web/app/_trpc/serverClient.ts | 4 - apps/web/app/_trpc/trpc-client.ts | 80 +++++++++++ apps/web/app/_trpc/trpc-provider.tsx | 130 +----------------- apps/web/app/_trpc/{client.ts => trpc.ts} | 2 + apps/web/app/layout.tsx | 7 +- apps/web/app/providers.tsx | 6 +- .../apps/categories/getServerSideProps.tsx | 6 +- apps/web/lib/apps/getServerSideProps.tsx | 6 +- apps/web/server/lib/ssg.ts | 52 ------- 14 files changed, 130 insertions(+), 240 deletions(-) delete mode 100644 apps/web/app/_trpc/HydrateClient.tsx delete mode 100644 apps/web/app/_trpc/createHydrateClient.tsx create mode 100644 apps/web/app/_trpc/query-client.ts delete mode 100644 apps/web/app/_trpc/serverClient.ts create mode 100644 apps/web/app/_trpc/trpc-client.ts rename apps/web/app/_trpc/{client.ts => trpc.ts} (92%) delete mode 100644 apps/web/server/lib/ssg.ts diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/event-types/page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/event-types/page.tsx index b54b052147..8a11ed2452 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/event-types/page.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/event-types/page.tsx @@ -8,8 +8,6 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { buildLegacyCtx } from "@lib/buildLegacyCtx"; -import { ssrInit } from "@server/lib/ssr"; - import EventTypes, { EventTypesCTA } from "~/event-types/views/event-types-listing-view"; export const generateMetadata = async () => @@ -26,7 +24,6 @@ const Page = async ({ params, searchParams }: PageProps) => { redirect("/auth/login"); } - await ssrInit(context); const t = await getTranslate(); return ( diff --git a/apps/web/app/(use-page-wrapper)/auth/logout/page.tsx b/apps/web/app/(use-page-wrapper)/auth/logout/page.tsx index f95c2e1957..f949498dfb 100644 --- a/apps/web/app/(use-page-wrapper)/auth/logout/page.tsx +++ b/apps/web/app/(use-page-wrapper)/auth/logout/page.tsx @@ -4,8 +4,6 @@ import { cookies, headers } from "next/headers"; import { buildLegacyCtx } from "@lib/buildLegacyCtx"; -import { ssrInit } from "@server/lib/ssr"; - import Logout from "~/auth/logout-view"; export const generateMetadata = async () => { @@ -19,7 +17,6 @@ const Page = async ({ params, searchParams }: PageProps) => { // cookie will be cleared in `/apps/web/middleware.ts` const h = await headers(); const context = buildLegacyCtx(h, await cookies(), await params, await searchParams); - await ssrInit(context); return ; }; diff --git a/apps/web/app/_trpc/HydrateClient.tsx b/apps/web/app/_trpc/HydrateClient.tsx deleted file mode 100644 index 16cbeb6b9a..0000000000 --- a/apps/web/app/_trpc/HydrateClient.tsx +++ /dev/null @@ -1,8 +0,0 @@ -"use client"; - -import { createHydrateClient } from "app/_trpc/createHydrateClient"; -import superjson from "superjson"; - -export const HydrateClient = createHydrateClient({ - transformer: superjson, -}); diff --git a/apps/web/app/_trpc/createHydrateClient.tsx b/apps/web/app/_trpc/createHydrateClient.tsx deleted file mode 100644 index 6751a02162..0000000000 --- a/apps/web/app/_trpc/createHydrateClient.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client"; - -import { type DehydratedState, HydrationBoundary } from "@tanstack/react-query"; -import { useMemo } from "react"; - -type DataTransformer = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - serialize: (data: any) => any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - deserialize: (data: any) => any; -}; - -export function createHydrateClient(opts: { transformer: DataTransformer }) { - return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) { - const { state, children } = props; - - const transformedState: DehydratedState = useMemo(() => { - if (opts.transformer) { - return opts.transformer.deserialize(state); - } - return state; - }, [state]); - - return {children}; - }; -} diff --git a/apps/web/app/_trpc/query-client.ts b/apps/web/app/_trpc/query-client.ts new file mode 100644 index 0000000000..369953c8f5 --- /dev/null +++ b/apps/web/app/_trpc/query-client.ts @@ -0,0 +1,37 @@ +"use client"; + +import { QueryClient } from "@tanstack/react-query"; + +import { TRPCClientError } from "@trpc/client"; + +const MAX_QUERY_RETRIES = 3; + +const isTRPCClientError = (cause: unknown): cause is TRPCClientError => { + return cause instanceof TRPCClientError; +}; + +export const queryClient = new QueryClient({ + // these configurations are copied from "packages/trpc/react/trpc.ts" + defaultOptions: { + queries: { + /** + * 1s should be enough to just keep identical query waterfalls low + * @example if one page components uses a query that is also used further down the tree + */ + staleTime: 1000, + /** + * Retry `useQuery()` calls depending on this function + */ + retry(failureCount, error) { + if (isTRPCClientError(error) && error.data) { + const { code } = error.data; + if (code === "BAD_REQUEST" || code === "FORBIDDEN" || code === "UNAUTHORIZED") { + // if input data is wrong or you're not authorized there's no point retrying a query + return false; + } + } + return failureCount < MAX_QUERY_RETRIES; + }, + }, + }, +}); diff --git a/apps/web/app/_trpc/serverClient.ts b/apps/web/app/_trpc/serverClient.ts deleted file mode 100644 index e4b7d577f0..0000000000 --- a/apps/web/app/_trpc/serverClient.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { TRPCContext } from "@calcom/trpc/server/createContext"; -import { appRouter } from "@calcom/trpc/server/routers/_app"; - -export const getServerCaller = (ctx: TRPCContext) => appRouter.createCaller(ctx); diff --git a/apps/web/app/_trpc/trpc-client.ts b/apps/web/app/_trpc/trpc-client.ts new file mode 100644 index 0000000000..3519c7b909 --- /dev/null +++ b/apps/web/app/_trpc/trpc-client.ts @@ -0,0 +1,80 @@ +"use client"; + +import superjson from "superjson"; + +import { ENDPOINTS } from "@calcom/trpc/react/shared"; + +import { httpBatchLink, httpLink, loggerLink, splitLink } from "@trpc/client"; + +import { trpc } from "./trpc"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const resolveEndpoint = (links: any) => { + // TODO: Update our trpc routes so they are more clear. + // This function parses paths like the following and maps them + // to the correct API endpoints. + // - viewer.me - 2 segment paths like this are for logged in requests + // - viewer.public.i18n - 3 segments paths can be public or authed + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (ctx: any) => { + const parts = ctx.op.path.split("."); + let endpoint; + let path = ""; + if (parts.length == 2) { + endpoint = parts[0] as keyof typeof links; + path = parts[1]; + } else { + endpoint = parts[1] as keyof typeof links; + path = parts.splice(2, parts.length - 2).join("."); + } + return links[endpoint]({ ...ctx, op: { ...ctx.op, path } }); + }; +}; + +const url = + typeof window !== "undefined" + ? "/api/trpc" + : process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}/api/trpc` + : `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`; + +export const trpcClient = trpc.createClient({ + links: [ + // adds pretty logs to your console in development and logs errors in production + loggerLink({ + enabled: (opts) => + (typeof process.env.NEXT_PUBLIC_LOGGER_LEVEL === "number" && + process.env.NEXT_PUBLIC_LOGGER_LEVEL >= 0) || + (opts.direction === "down" && opts.result instanceof Error), + }), + splitLink({ + // check for context property `skipBatch` + condition: (op) => !!op.context.skipBatch, + // when condition is true, use normal request + true: (runtime) => { + const links = Object.fromEntries( + ENDPOINTS.map((endpoint) => [ + endpoint, + httpLink({ + url: `${url}/${endpoint}`, + })(runtime), + ]) + ); + return resolveEndpoint(links); + }, + // when condition is false, use batch request + false: (runtime) => { + const links = Object.fromEntries( + ENDPOINTS.map((endpoint) => [ + endpoint, + httpBatchLink({ + url: `${url}/${endpoint}`, + })(runtime), + ]) + ); + return resolveEndpoint(links); + }, + }), + ], + transformer: superjson, +}); diff --git a/apps/web/app/_trpc/trpc-provider.tsx b/apps/web/app/_trpc/trpc-provider.tsx index 9c0cbcd048..1c4af7b072 100644 --- a/apps/web/app/_trpc/trpc-provider.tsx +++ b/apps/web/app/_trpc/trpc-provider.tsx @@ -1,137 +1,19 @@ "use client"; -import { type DehydratedState, QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { HydrateClient } from "app/_trpc/HydrateClient"; -import { trpc } from "app/_trpc/client"; -import { useState } from "react"; -import superjson from "superjson"; +import { QueryClientProvider } from "@tanstack/react-query"; -import { ENDPOINTS } from "@calcom/trpc/react/shared"; -import type { AppRouter } from "@calcom/trpc/types/server/routers/_app"; - -import { httpBatchLink, httpLink, loggerLink, splitLink, TRPCClientError } from "@trpc/client"; - -export type Endpoint = (typeof ENDPOINTS)[number]; - -const MAX_QUERY_RETRIES = 3; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const resolveEndpoint = (links: any) => { - // TODO: Update our trpc routes so they are more clear. - // This function parses paths like the following and maps them - // to the correct API endpoints. - // - viewer.me - 2 segment paths like this are for logged in requests - // - viewer.public.i18n - 3 segments paths can be public or authed - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (ctx: any) => { - const parts = ctx.op.path.split("."); - let endpoint; - let path = ""; - if (parts.length == 2) { - endpoint = parts[0] as keyof typeof links; - path = parts[1]; - } else { - endpoint = parts[1] as keyof typeof links; - path = parts.splice(2, parts.length - 2).join("."); - } - return links[endpoint]({ ...ctx, op: { ...ctx.op, path } }); - }; -}; - -const isTRPCClientError = (cause: unknown): cause is TRPCClientError => { - return cause instanceof TRPCClientError; -}; +import { queryClient } from "./query-client"; +import { trpc } from "./trpc"; +import { trpcClient } from "./trpc-client"; type Props = { children: React.ReactNode; - dehydratedState: DehydratedState; }; -export const TrpcProvider = ({ children, dehydratedState }: Props) => { - const [queryClient] = useState( - () => - new QueryClient({ - // these configurations are copied from "packages/trpc/react/trpc.ts" - defaultOptions: { - queries: { - /** - * 1s should be enough to just keep identical query waterfalls low - * @example if one page components uses a query that is also used further down the tree - */ - staleTime: 1000, - /** - * Retry `useQuery()` calls depending on this function - */ - retry(failureCount, error) { - if (isTRPCClientError(error) && error.data) { - const { code } = error.data; - if (code === "BAD_REQUEST" || code === "FORBIDDEN" || code === "UNAUTHORIZED") { - // if input data is wrong or you're not authorized there's no point retrying a query - return false; - } - } - return failureCount < MAX_QUERY_RETRIES; - }, - }, - }, - }) - ); - - const url = - typeof window !== "undefined" - ? "/api/trpc" - : process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}/api/trpc` - : `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`; - - const [trpcClient] = useState(() => - trpc.createClient({ - links: [ - // adds pretty logs to your console in development and logs errors in production - loggerLink({ - enabled: (opts) => - (typeof process.env.NEXT_PUBLIC_LOGGER_LEVEL === "number" && - process.env.NEXT_PUBLIC_LOGGER_LEVEL >= 0) || - (opts.direction === "down" && opts.result instanceof Error), - }), - splitLink({ - // check for context property `skipBatch` - condition: (op) => !!op.context.skipBatch, - // when condition is true, use normal request - true: (runtime) => { - const links = Object.fromEntries( - ENDPOINTS.map((endpoint) => [ - endpoint, - httpLink({ - url: `${url}/${endpoint}`, - })(runtime), - ]) - ); - return resolveEndpoint(links); - }, - // when condition is false, use batch request - false: (runtime) => { - const links = Object.fromEntries( - ENDPOINTS.map((endpoint) => [ - endpoint, - httpBatchLink({ - url: `${url}/${endpoint}`, - })(runtime), - ]) - ); - return resolveEndpoint(links); - }, - }), - ], - transformer: superjson, - }) - ); - +export const TrpcProvider = ({ children }: Props) => { return ( - - {children} - + {children} ); }; diff --git a/apps/web/app/_trpc/client.ts b/apps/web/app/_trpc/trpc.ts similarity index 92% rename from apps/web/app/_trpc/client.ts rename to apps/web/app/_trpc/trpc.ts index 978570ed05..c7a27cbef7 100644 --- a/apps/web/app/_trpc/client.ts +++ b/apps/web/app/_trpc/trpc.ts @@ -1,3 +1,5 @@ +"use client"; + import type { AppRouter } from "@calcom/trpc/types/server/routers/_app"; import { createTRPCReact } from "@trpc/react-query"; diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 6707cc6e69..965588fb92 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -8,10 +8,6 @@ import { getLocale } from "@calcom/features/auth/lib/getLocale"; import { IconSprites } from "@calcom/ui/components/icon"; import { NotificationSoundHandler } from "@calcom/web/components/notification-sound-handler"; -import { buildLegacyCtx } from "@lib/buildLegacyCtx"; - -import { ssrInit } from "@server/lib/ssr"; - import "../styles/globals.css"; import { SpeculationRules } from "./SpeculationRules"; import { Providers } from "./providers"; @@ -112,7 +108,6 @@ export default async function RootLayout({ children }: { children: React.ReactNo ? getFallbackProps() : await getInitialProps(fullUrl); - const ssr = await ssrInit(buildLegacyCtx(h, await cookies(), {}, {})); return ( - {children} + {children} {!isEmbed && } diff --git a/apps/web/app/providers.tsx b/apps/web/app/providers.tsx index 2b3a5caad2..aaca3b3dc6 100644 --- a/apps/web/app/providers.tsx +++ b/apps/web/app/providers.tsx @@ -1,6 +1,5 @@ "use client"; -import { type DehydratedState } from "@tanstack/react-query"; import { TrpcProvider } from "app/_trpc/trpc-provider"; import { SessionProvider } from "next-auth/react"; import CacheProvider from "react-inlinesvg/provider"; @@ -12,14 +11,13 @@ import PlainChat from "@lib/plain/dynamicProvider"; type ProvidersProps = { children: React.ReactNode; - dehydratedState: DehydratedState; }; -export function Providers({ children, dehydratedState }: ProvidersProps) { +export function Providers({ children }: ProvidersProps) { const isBookingPage = useIsBookingPage(); return ( - + {!isBookingPage ? : null} {/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */} diff --git a/apps/web/lib/apps/categories/getServerSideProps.tsx b/apps/web/lib/apps/categories/getServerSideProps.tsx index db3f86519c..b8d9a058de 100644 --- a/apps/web/lib/apps/categories/getServerSideProps.tsx +++ b/apps/web/lib/apps/categories/getServerSideProps.tsx @@ -3,13 +3,9 @@ import type { GetServerSidePropsContext } from "next"; import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { ssrInit } from "@server/lib/ssr"; - export const getServerSideProps = async (context: GetServerSidePropsContext) => { const { req } = context; - const ssr = await ssrInit(context); - const session = await getServerSession({ req }); let appStore; @@ -29,7 +25,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => return { props: { categories: Object.entries(categories).map(([name, count]) => ({ name, count })), - trpcState: ssr.dehydrate(), + trpcState: undefined, }, }; }; diff --git a/apps/web/lib/apps/getServerSideProps.tsx b/apps/web/lib/apps/getServerSideProps.tsx index 7288401ee7..bf2cffa4c5 100644 --- a/apps/web/lib/apps/getServerSideProps.tsx +++ b/apps/web/lib/apps/getServerSideProps.tsx @@ -5,13 +5,9 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { UserRepository } from "@calcom/lib/server/repository/user"; import type { AppCategories } from "@calcom/prisma/enums"; -import { ssrInit } from "@server/lib/ssr"; - export const getServerSideProps = async (context: GetServerSidePropsContext) => { const { req } = context; - const ssr = await ssrInit(context); - const session = await getServerSession({ req }); let appStore, userAdminTeamsIds: number[]; @@ -46,7 +42,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => }), appStore, userAdminTeams: userAdminTeamsIds, - trpcState: ssr.dehydrate(), + trpcState: undefined, }, }; }; diff --git a/apps/web/server/lib/ssg.ts b/apps/web/server/lib/ssg.ts deleted file mode 100644 index 1d1fa30467..0000000000 --- a/apps/web/server/lib/ssg.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { GetStaticPropsContext } from "next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import superjson from "superjson"; - -import { CALCOM_VERSION } from "@calcom/lib/constants"; -import prisma, { readonlyPrisma } from "@calcom/prisma"; -import { appRouter } from "@calcom/trpc/server/routers/_app"; - -import { createServerSideHelpers } from "@trpc/react-query/server"; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const { i18n } = require("@calcom/config/next-i18next.config"); - -/** - * Initialize static site rendering tRPC helpers. - * Provides a method to prefetch tRPC-queries in a `getStaticProps`-function. - * Automatically prefetches i18n based on the passed in `context`-object to prevent i18n-flickering. - * Make sure to `return { props: { trpcState: ssr.dehydrate() } }` at the end. - */ -export async function ssgInit(opts: GetStaticPropsContext) { - const requestedLocale = opts.params?.locale || opts.locale || i18n.defaultLocale; - const isSupportedLocale = i18n.locales.includes(requestedLocale); - if (!isSupportedLocale) { - console.warn(`Requested unsupported locale "${requestedLocale}"`); - } - const locale = isSupportedLocale ? requestedLocale : i18n.defaultLocale; - - const _i18n = await serverSideTranslations(locale, ["common"]); - - const ssg = createServerSideHelpers({ - router: appRouter, - transformer: superjson, - ctx: { - prisma, - insightsDb: readonlyPrisma, - session: null, - locale, - i18n: _i18n, - }, - }); - - // i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch - // we can set query data directly to the queryClient - const queryKey = [ - ["viewer", "public", "i18n"], - { input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" }, - ]; - - ssg.queryClient.setQueryData(queryKey, { i18n: _i18n }); - - return ssg; -}