From bd2a24df2b9f7f6aefdc0ea685957a1ca4983377 Mon Sep 17 00:00:00 2001 From: stroblme Date: Wed, 28 Oct 2026 18:02:06 +0100 Subject: [PATCH] Entry chunk: signup and the form schemas leave it, build runs without warnings The entry is 798 kB (259 kB gzipped) instead of 927 kB (298 kB). Signup's `export default` kept it unsplit, and login, recover and reset-password left a bare `z.object(...)` statement in their route stubs (reset-password also read its search through Zod), which held all of Zod and react-hook-form in the entry. The schemas are factories now, reset-password validates its search by hand. The shell routes keep their component in the entry on purpose (`codeSplitGroupings: []`, no `export default`): splitting them out measured about 60 chunks per signed-in screen and a slower flow list. The Zod `@__PURE__` comment warning is filtered and the chunk limit is 850 kB, each with a comment saying why. At 4x CPU on slow 4G the signed-in screens load 5 to 9% sooner, view 6%; login 13% later (the form chunk is one more hop). On a LAN they are within noise. Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh --- frontend/src/routes/_canvas.tsx | 5 +-- frontend/src/routes/_layout.tsx | 5 +-- frontend/src/routes/login.tsx | 21 +++++++----- frontend/src/routes/recover-password.tsx | 13 ++++--- frontend/src/routes/reset-password.tsx | 43 +++++++++++++----------- frontend/src/routes/signup.tsx | 41 +++++++++++----------- frontend/vite.config.ts | 17 ++++++++++ 7 files changed, 90 insertions(+), 55 deletions(-) diff --git a/frontend/src/routes/_canvas.tsx b/frontend/src/routes/_canvas.tsx index 997c4a1..deabba9 100644 --- a/frontend/src/routes/_canvas.tsx +++ b/frontend/src/routes/_canvas.tsx @@ -16,6 +16,9 @@ import { isLoggedIn } from "@/hooks/useAuth" */ export const Route = createFileRoute("/_canvas")({ component: CanvasLayout, + // Every screen a signed-in user opens has this shell, so it stays in the + // entry chunk. Split off, it arrives a round trip later in dozens of chunks. + codeSplitGroupings: [], beforeLoad: async () => { if (!isLoggedIn()) { throw redirect({ to: "/login" }) @@ -35,5 +38,3 @@ function CanvasLayout() { ) } - -export default CanvasLayout diff --git a/frontend/src/routes/_layout.tsx b/frontend/src/routes/_layout.tsx index b395b86..b911f5c 100644 --- a/frontend/src/routes/_layout.tsx +++ b/frontend/src/routes/_layout.tsx @@ -20,6 +20,9 @@ import { isLoggedIn } from "@/hooks/useAuth" export const Route = createFileRoute("/_layout")({ component: Layout, + // Every screen a signed-in user opens has this shell, so it stays in the + // entry chunk. Split off, it arrives a round trip later in dozens of chunks. + codeSplitGroupings: [], // Inside the shell rather than at the root, so a screen that throws leaves // the sidebar standing and somebody can navigate away from it. The root's // own boundary replaces the whole app, navigation included. @@ -66,5 +69,3 @@ function Layout() { ) } - -export default Layout diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx index f4eb512..91c95fe 100644 --- a/frontend/src/routes/login.tsx +++ b/frontend/src/routes/login.tsx @@ -4,6 +4,7 @@ import { Link as RouterLink, redirect, } from "@tanstack/react-router" +import { useMemo } from "react" import { useForm } from "react-hook-form" import { z } from "zod" @@ -26,15 +27,18 @@ import useAuth, { useSignupEnabled, } from "@/hooks/useAuth" -const formSchema = z.object({ - username: z.email(), - password: z - .string() - .min(1, { message: "Password is required" }) - .min(8, { message: "Password must be at least 8 characters" }), -}) satisfies z.ZodType +// A function, not a constant: TanStack Router leaves a module-level +// `z.object(...)` statement behind in the entry chunk, and all of Zod with it. +const makeFormSchema = () => + z.object({ + username: z.email(), + password: z + .string() + .min(1, { message: "Password is required" }) + .min(8, { message: "Password must be at least 8 characters" }), + }) satisfies z.ZodType -type FormData = z.infer +type FormData = z.infer> export const Route = createFileRoute("/login")({ component: Login, @@ -60,6 +64,7 @@ export const Route = createFileRoute("/login")({ function Login() { const { loginMutation } = useAuth() const signupEnabled = useSignupEnabled() + const formSchema = useMemo(makeFormSchema, []) const form = useForm({ resolver: zodResolver(formSchema), mode: "onBlur", diff --git a/frontend/src/routes/recover-password.tsx b/frontend/src/routes/recover-password.tsx index 291d930..6d95f00 100644 --- a/frontend/src/routes/recover-password.tsx +++ b/frontend/src/routes/recover-password.tsx @@ -5,6 +5,7 @@ import { Link as RouterLink, redirect, } from "@tanstack/react-router" +import { useMemo } from "react" import { useForm } from "react-hook-form" import { z } from "zod" @@ -24,11 +25,14 @@ import { isLoggedIn } from "@/hooks/useAuth" import useCustomToast from "@/hooks/useCustomToast" import { handleError } from "@/utils" -const formSchema = z.object({ - email: z.email(), -}) +// A function, not a constant: TanStack Router leaves a module-level +// `z.object(...)` statement behind in the entry chunk, and all of Zod with it. +const makeFormSchema = () => + z.object({ + email: z.email(), + }) -type FormData = z.infer +type FormData = z.infer> export const Route = createFileRoute("/recover-password")({ component: RecoverPassword, @@ -49,6 +53,7 @@ export const Route = createFileRoute("/recover-password")({ }) function RecoverPassword() { + const formSchema = useMemo(makeFormSchema, []) const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { diff --git a/frontend/src/routes/reset-password.tsx b/frontend/src/routes/reset-password.tsx index 6183204..2cc456e 100644 --- a/frontend/src/routes/reset-password.tsx +++ b/frontend/src/routes/reset-password.tsx @@ -6,6 +6,7 @@ import { redirect, useNavigate, } from "@tanstack/react-router" +import { useMemo } from "react" import { useForm } from "react-hook-form" import { z } from "zod" @@ -25,30 +26,31 @@ import { isLoggedIn } from "@/hooks/useAuth" import useCustomToast from "@/hooks/useCustomToast" import { handleError } from "@/utils" -const searchSchema = z.object({ - token: z.string().catch(""), -}) +// A function, not a constant: TanStack Router leaves a module-level +// `z.object(...)` statement behind in the entry chunk, and all of Zod with it. +const makeFormSchema = () => + z + .object({ + new_password: z + .string() + .min(1, { message: "Password is required" }) + .min(8, { message: "Password must be at least 8 characters" }), + confirm_password: z + .string() + .min(1, { message: "Password confirmation is required" }), + }) + .refine((data) => data.new_password === data.confirm_password, { + message: "The passwords don't match", + path: ["confirm_password"], + }) -const formSchema = z - .object({ - new_password: z - .string() - .min(1, { message: "Password is required" }) - .min(8, { message: "Password must be at least 8 characters" }), - confirm_password: z - .string() - .min(1, { message: "Password confirmation is required" }), - }) - .refine((data) => data.new_password === data.confirm_password, { - message: "The passwords don't match", - path: ["confirm_password"], - }) - -type FormData = z.infer +type FormData = z.infer> export const Route = createFileRoute("/reset-password")({ component: ResetPassword, - validateSearch: searchSchema, + validateSearch: (search: Record): { token: string } => ({ + token: typeof search.token === "string" ? search.token : "", + }), beforeLoad: async ({ search }) => { if (isLoggedIn()) { throw redirect({ to: "/" }) @@ -71,6 +73,7 @@ function ResetPassword() { const { showSuccessToast, showErrorToast } = useCustomToast() const navigate = useNavigate() + const formSchema = useMemo(makeFormSchema, []) const form = useForm({ resolver: zodResolver(formSchema), mode: "onBlur", diff --git a/frontend/src/routes/signup.tsx b/frontend/src/routes/signup.tsx index e2e681b..f661efd 100644 --- a/frontend/src/routes/signup.tsx +++ b/frontend/src/routes/signup.tsx @@ -4,6 +4,7 @@ import { Link as RouterLink, redirect, } from "@tanstack/react-router" +import { useMemo } from "react" import { useForm } from "react-hook-form" import { z } from "zod" import { AuthLayout } from "@/components/Common/AuthLayout" @@ -20,24 +21,27 @@ import { LoadingButton } from "@/components/ui/loading-button" import { PasswordInput } from "@/components/ui/password-input" import useAuth, { isLoggedIn, useSignupEnabled } from "@/hooks/useAuth" -const formSchema = z - .object({ - email: z.email(), - full_name: z.string().min(1, { message: "Full Name is required" }), - password: z - .string() - .min(1, { message: "Password is required" }) - .min(8, { message: "Password must be at least 8 characters" }), - confirm_password: z - .string() - .min(1, { message: "Password confirmation is required" }), - }) - .refine((data) => data.password === data.confirm_password, { - message: "The passwords don't match", - path: ["confirm_password"], - }) +// A function, not a constant: TanStack Router leaves a module-level +// `z.object(...)` statement behind in the entry chunk, and all of Zod with it. +const makeFormSchema = () => + z + .object({ + email: z.email(), + full_name: z.string().min(1, { message: "Full Name is required" }), + password: z + .string() + .min(1, { message: "Password is required" }) + .min(8, { message: "Password must be at least 8 characters" }), + confirm_password: z + .string() + .min(1, { message: "Password confirmation is required" }), + }) + .refine((data) => data.password === data.confirm_password, { + message: "The passwords don't match", + path: ["confirm_password"], + }) -type FormData = z.infer +type FormData = z.infer> export const Route = createFileRoute("/signup")({ component: SignUp, @@ -60,6 +64,7 @@ export const Route = createFileRoute("/signup")({ function SignUp() { const { signUpMutation } = useAuth() const signupEnabled = useSignupEnabled() + const formSchema = useMemo(makeFormSchema, []) const form = useForm({ resolver: zodResolver(formSchema), mode: "onBlur", @@ -210,5 +215,3 @@ function SignUp() { ) } - -export default SignUp diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index f61a045..0b7ae83 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -15,6 +15,23 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, + build: { + // The entry holds React, the router, the query client and the shell, and + // cannot get below this. A bigger chunk than that is worth a warning. + chunkSizeWarningLimit: 850, + rollupOptions: { + onwarn(warning, defaultHandler) { + // Zod's own comments name `@__PURE__`, which Rollup reads as a + // misplaced annotation. Nothing to fix on our side. + if ( + warning.code === "INVALID_ANNOTATION" && + warning.id?.includes("/node_modules/zod/") + ) + return + defaultHandler(warning) + }, + }, + }, plugins: [ tanstackRouter({ target: "react",