Skip to content

Commit 0301ec1

Browse files
committed
address performance review followups
1 parent 5586b6f commit 0301ec1

7 files changed

Lines changed: 52 additions & 76 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ These instructions apply to the whole repository.
1717
## Auth, Sessions, and Public-Page Performance
1818

1919
- `src/proxy.ts` intentionally does not refresh Supabase auth on every request. Public pages should use `createSignedOutResponse()` so their initial response does not wait on `supabase.auth.getUser()`.
20-
- Only add paths to `authRequiredPathPrefixes` when the first server response must know auth state, such as protected routes, auth callbacks, and guest-only auth pages. Adding public routes there can regress TTFB, FCP, and LCP.
20+
- Only add paths to `authRequiredPathPrefixes` when the first server response must know auth state, such as protected routes, auth callbacks, guest-only auth pages, or routes that server-render private/public data differently. `/map` and `/listings` belong there because they call server `auth.getUser()` before choosing listing data sources.
2121
- Server components that branch on auth should treat `authStateHeaderName` as a forwarded proxy hint, not proof that public routes have performed a fresh auth lookup. Public pages are deliberately signed-out on the initial server render until client auth resolves.
2222
- Keep auth-aware public UI in small client slots or enhancements, such as `AccountButton`, `FooterLocaleSlot`, and unread chat dots. It is acceptable for these to appear or update after first paint.
2323
- Keep `UnreadMessagesProvider` scoped to tab-bar and chat layouts rather than the root layout. It should not make public HTML wait on Supabase, and its initial auth/unread check should remain idle or otherwise deferred.

docs/auth-session-architecture.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ Peels keeps public pages fast by avoiding a server-side Supabase auth refresh un
88

99
- Auth-required paths use `updateSession()`. This creates a Supabase server client, calls `supabase.auth.getUser()`, refreshes cookies when needed, forwards `x-peels-auth-state`, and applies auth redirects for protected or guest-only pages.
1010
- Public paths use `createSignedOutResponse()`. This forwards the current path and a signed-out auth hint without calling Supabase, so pages such as `/`, `/share`, and static content do not block first paint on auth.
11-
- `authRequiredPathPrefixes` should stay small. Add to it only when the first server response truly needs auth state.
11+
- `authRequiredPathPrefixes` should stay small. Add to it only when the first server response truly needs auth state. `/map` and `/listings` belong there because they server-render auth-aware listing data before client hydration.
1212

13-
The forwarded auth state is a rendering hint. On public routes, it intentionally says signed-out on the initial server render even if the browser has a valid session cookie. Client-side auth slots can then resolve the real state after hydration.
13+
The forwarded auth state is a rendering hint. On public routes that do not need server auth, it intentionally says signed-out on the initial server render even if the browser has a valid session cookie. Client-side auth slots can then resolve the real state after hydration.
1414

1515
## Locale Behaviour
1616

@@ -38,7 +38,7 @@ The unread check should stay deferred so it does not delay public HTML. If the u
3838

3939
The homepage should server-render useful static content first, then hydrate dynamism later.
4040

41-
- `IntroHeader` owns the static hero frame and primary calls to action.
41+
- `IntroHeader` reserves the hero visual space without server-rendering the decorative map/avatar/pin frame.
4242
- `DeferredIntroHeaderRotator` loads the animated hero rotator after the first paint/idle window.
4343
- `PeelsHowItWorks` keeps crawlable explanatory content in the initial HTML.
4444
- Deferred demo components load map, listing, chat, and photo demos after intersection or idle.

src/components/DeferredIntroHeaderRotator/DeferredIntroHeaderRotator.tsx

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,24 @@
22

33
import dynamic from "next/dynamic";
44
import { useEffect, useState } from "react";
5-
6-
type IdleWindow = Window & {
7-
requestIdleCallback?: (
8-
callback: () => void,
9-
options?: { timeout?: number }
10-
) => number;
11-
cancelIdleCallback?: (handle: number) => void;
12-
};
5+
import { scheduleIdleTask } from "@/utils/scheduleIdleTask";
136

147
const IntroHeaderRotator = dynamic(
158
() => import("@/components/IntroHeader/IntroHeaderRotator"),
169
{ ssr: false }
1710
);
1811

19-
function scheduleIdleTask(callback: () => void) {
20-
const idleWindow = window as IdleWindow;
21-
22-
if (typeof idleWindow.requestIdleCallback === "function") {
23-
const idleCallbackId = idleWindow.requestIdleCallback(callback, {
24-
timeout: 1_500,
25-
});
26-
27-
return () => idleWindow.cancelIdleCallback?.(idleCallbackId);
28-
}
29-
30-
const timeoutId = globalThis.setTimeout(callback, 250);
31-
return () => globalThis.clearTimeout(timeoutId);
32-
}
33-
3412
export default function DeferredIntroHeaderRotator() {
3513
const [isReady, setIsReady] = useState(false);
3614

37-
useEffect(() => scheduleIdleTask(() => setIsReady(true)), []);
15+
useEffect(
16+
() =>
17+
scheduleIdleTask(() => setIsReady(true), {
18+
timeout: 1_500,
19+
fallbackDelay: 250,
20+
}),
21+
[]
22+
);
3823

3924
return isReady ? <IntroHeaderRotator /> : null;
4025
}

src/contexts/UnreadMessagesContext.tsx

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { createClient } from "@/utils/supabase/client";
1212
import { usePathname } from "next/navigation";
1313
import { getChatThreadIdFromPathname } from "@/features/chat/chatRoutes";
14+
import { scheduleIdleTask } from "@/utils/scheduleIdleTask";
1415
import type { Dispatch, PropsWithChildren, SetStateAction } from "react";
1516

1617
type ThreadReadStatus = Record<string, boolean>;
@@ -34,29 +35,6 @@ const UnreadMessagesContext = createContext<
3435
>(undefined);
3536
const isAuthDebugEnabled = process.env.NEXT_PUBLIC_AUTH_DEBUG === "true";
3637

37-
type IdleWindow = Window & {
38-
requestIdleCallback?: (
39-
callback: () => void,
40-
options?: { timeout?: number }
41-
) => number;
42-
cancelIdleCallback?: (handle: number) => void;
43-
};
44-
45-
function scheduleIdleTask(callback: () => void) {
46-
const idleWindow = window as IdleWindow;
47-
48-
if (typeof idleWindow.requestIdleCallback === "function") {
49-
const idleCallbackId = idleWindow.requestIdleCallback(callback, {
50-
timeout: 2_000,
51-
});
52-
53-
return () => idleWindow.cancelIdleCallback?.(idleCallbackId);
54-
}
55-
56-
const timeoutId = globalThis.setTimeout(callback, 250);
57-
return () => globalThis.clearTimeout(timeoutId);
58-
}
59-
6038
export function UnreadMessagesProvider({ children }: PropsWithChildren) {
6139
const [unreadCount, setUnreadCount] = useState(0);
6240
const [threadReadStatus, setThreadReadStatus] = useState<ThreadReadStatus>(

src/hooks/useDeferredHomepageDemo.ts

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,7 @@
11
"use client";
22

33
import { useEffect, useRef, useState } from "react";
4-
5-
type IdleWindow = Window & {
6-
requestIdleCallback?: (
7-
callback: () => void,
8-
options?: { timeout?: number }
9-
) => number;
10-
cancelIdleCallback?: (handle: number) => void;
11-
};
12-
13-
function scheduleIdleTask(callback: () => void) {
14-
const idleWindow = window as IdleWindow;
15-
16-
if (typeof idleWindow.requestIdleCallback === "function") {
17-
const idleCallbackId = idleWindow.requestIdleCallback(callback, {
18-
timeout: 2_000,
19-
});
20-
21-
return () => idleWindow.cancelIdleCallback?.(idleCallbackId);
22-
}
23-
24-
const timeoutId = globalThis.setTimeout(callback, 300);
25-
return () => globalThis.clearTimeout(timeoutId);
26-
}
4+
import { scheduleIdleTask } from "@/utils/scheduleIdleTask";
275

286
export function useDeferredHomepageDemo() {
297
const [isReady, setIsReady] = useState(false);
@@ -35,7 +13,10 @@ export function useDeferredHomepageDemo() {
3513
}
3614

3715
const markReady = () => setIsReady(true);
38-
const cancelIdleTask = scheduleIdleTask(markReady);
16+
const cancelIdleTask = scheduleIdleTask(markReady, {
17+
timeout: 2_000,
18+
fallbackDelay: 300,
19+
});
3920
const observer =
4021
"IntersectionObserver" in window
4122
? new IntersectionObserver(

src/proxy.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const authRequiredPathPrefixes = [
3333
"/auth",
3434
"/chats",
3535
"/forgot-password",
36+
"/listings",
37+
"/map",
3638
"/profile",
3739
"/sign-in",
3840
"/sign-up",
@@ -46,7 +48,7 @@ function shouldUpdateSession(request: NextRequest) {
4648

4749
export async function proxy(request: NextRequest) {
4850
// Public routes deliberately skip Supabase auth refresh for first-paint
49-
// performance. Auth-aware client slots converge after hydration.
51+
// performance. Routes that server-render auth-aware data stay above.
5052
const response = shouldUpdateSession(request)
5153
? await updateSession(request)
5254
: createSignedOutResponse(request);

src/utils/scheduleIdleTask.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
type IdleWindow = Window & {
2+
requestIdleCallback?: (
3+
callback: () => void,
4+
options?: { timeout?: number }
5+
) => number;
6+
cancelIdleCallback?: (handle: number) => void;
7+
};
8+
9+
type ScheduleIdleTaskOptions = {
10+
timeout?: number;
11+
fallbackDelay?: number;
12+
};
13+
14+
export function scheduleIdleTask(
15+
callback: () => void,
16+
{ timeout = 2_000, fallbackDelay = 250 }: ScheduleIdleTaskOptions = {}
17+
) {
18+
const idleWindow = window as IdleWindow;
19+
20+
if (typeof idleWindow.requestIdleCallback === "function") {
21+
const idleCallbackId = idleWindow.requestIdleCallback(callback, {
22+
timeout,
23+
});
24+
25+
return () => idleWindow.cancelIdleCallback?.(idleCallbackId);
26+
}
27+
28+
const timeoutId = globalThis.setTimeout(callback, fallbackDelay);
29+
return () => globalThis.clearTimeout(timeoutId);
30+
}

0 commit comments

Comments
 (0)