Skip to content

Commit 2539d9b

Browse files
committed
add cron job
1 parent 75d529e commit 2539d9b

4 files changed

Lines changed: 232 additions & 1 deletion

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Performance Report — codersforcauses.org
2+
3+
> Generated: 2026-02-26
4+
> Tool: Chrome DevTools MCP (no CPU/network throttling, lab conditions)
5+
> No CrUX field data available for this site.
6+
7+
---
8+
9+
## Summary
10+
11+
| Page | LCP | CLS | Status |
12+
| ------------------------------------ | -------- | -------- | ------------- |
13+
| Homepage (`/`) | 463 ms | 0.00 | Good |
14+
| User Dashboard (`/dashboard`) | 463 ms\* | 0.00\* | Good |
15+
| Admin Dashboard (`/dashboard/admin`) | 827 ms | **0.25** | CLS needs fix |
16+
17+
> \* Dashboard page not independently traced — estimated similar to homepage.
18+
19+
---
20+
21+
## Page 1: Homepage (`/`)
22+
23+
### Core Web Vitals
24+
25+
| Metric | Score | Threshold |
26+
| ------ | ------ | --------------- |
27+
| LCP | 463 ms | Good (<2,500ms) |
28+
| CLS | 0.00 | Good (<0.1) |
29+
30+
### LCP Breakdown
31+
32+
The LCP element is a `<p>` text node — no network fetch needed.
33+
34+
| Phase | Time | % of LCP |
35+
| -------------------- | ---------- | --------- |
36+
| TTFB | 53 ms | 11.5% |
37+
| Element Render Delay | **410 ms** | **88.5%** |
38+
39+
The large render delay is caused by Next.js JS hydration blocking text rendering before it becomes the LCP element.
40+
41+
### Issues
42+
43+
#### 1. JS Hydration Render Delay (410ms)
44+
45+
LCP text is not painted until after JS hydration completes.
46+
47+
**Fix:** Ensure the LCP text is present in the initial SSR HTML and is not dependent on client-side JS to render.
48+
49+
#### 2. Network Dependency Chain (critical path: 567ms)
50+
51+
```
52+
HTML → CSS (a25ca756ef64d4e8.css, 400ms)
53+
└── Font (material-symbols-sharp.fcbbf02a.woff2, 411ms)
54+
```
55+
56+
The font is only discovered after CSS is parsed. No `preconnect` hints configured.
57+
58+
**Fix:** Add a preload hint in `<head>` to break the chain:
59+
60+
```html
61+
<link
62+
rel="preload"
63+
href="/_next/static/media/material-symbols-sharp.fcbbf02a.woff2"
64+
as="font"
65+
type="font/woff2"
66+
crossorigin
67+
/>
68+
```
69+
70+
#### 3. Unsized Client Logo Images (CLS: 0.0038 — minor but recurring)
71+
72+
The logo carousel triggers 34 micro layout shifts (589ms–4,134ms) from unsized `<img>` elements:
73+
74+
| Image | Shifts |
75+
| --------------------- | ------ |
76+
| `foodbank_logo.svg` | 1 |
77+
| `csf_logo_dark.svg` | 26 |
78+
| `repair_lab_logo.png` | 7 |
79+
80+
**Fix:** Add explicit `width` and `height` attributes to carousel `<img>` tags:
81+
82+
```html
83+
<img
84+
src="/clients/csf_logo_dark.svg"
85+
width="160"
86+
height="60"
87+
class="brightness-110 contrast-[0.2] grayscale transition duration-300"
88+
/>
89+
```
90+
91+
Or use CSS `aspect-ratio` to reserve space before images load.
92+
93+
---
94+
95+
## Page 2: Admin Dashboard (`/dashboard/admin`)
96+
97+
### Core Web Vitals
98+
99+
| Metric | Score | Threshold |
100+
| ------ | -------- | ------------------------------- |
101+
| LCP | 827 ms | Good (<2,500ms) |
102+
| CLS | **0.25** | **Bad (>0.1, at Bad boundary)** |
103+
104+
### LCP Breakdown
105+
106+
The LCP element is the footer logo `cfc_logo_white_full.svg` — unusual, as the actual page content (user data table) is not the LCP element, suggesting the table content is rendered client-side.
107+
108+
| Phase | Time | % of LCP |
109+
| ---------------------- | ---------- | --------- |
110+
| TTFB | 55 ms | 6.6% |
111+
| Resource Load Delay | **667 ms** | **80.6%** |
112+
| Resource Load Duration | 57 ms | 6.9% |
113+
| Element Render Delay | 49 ms | 5.9% |
114+
115+
### Issues
116+
117+
#### 1. CLS: 0.25 — CRITICAL
118+
119+
A single layout shift of score **0.2482** at 1,308ms dominates. Root cause: the footer logo `cfc_logo_white_full.svg` has no `width`/`height` attributes.
120+
121+
**Fix:** Add explicit dimensions to the footer logo:
122+
123+
```html
124+
<!-- In the footer component -->
125+
<img
126+
src="/logo/cfc_logo_white_full.svg"
127+
class="!w-auto object-top md:!h-auto"
128+
width="200"
129+
height="50"
130+
alt="Coders for Causes logo"
131+
/>
132+
```
133+
134+
#### 2. LCP Image: Missing `fetchpriority=high` + Lazy Loading Applied
135+
136+
The LCP image (`cfc_logo_white_full.svg`) failed two discovery checks:
137+
138+
- `fetchpriority=high` not set
139+
- `loading="lazy"` is applied (delays the LCP image)
140+
141+
**Fix:**
142+
143+
```html
144+
<img
145+
src="/logo/cfc_logo_white_full.svg"
146+
fetchpriority="high"
147+
loading="eager" <!-- remove lazy loading from LCP images -->
148+
width="200"
149+
height="50"
150+
/>
151+
```
152+
153+
#### 3. LCP Resource Load Delay (667ms — 80.6% of LCP)
154+
155+
The logo image isn't requested until 722ms after navigation starts. The browser can't discover it until late in page load.
156+
157+
**Fix:** Add a `preload` hint for the logo if it consistently appears as the LCP element:
158+
159+
```html
160+
<link rel="preload" href="/logo/cfc_logo_white_full.svg" as="image" />
161+
```
162+
163+
#### 4. Aggressive Cache Revalidation on Static Asset
164+
165+
The logo SVG returns `cache-control: public, max-age=0, must-revalidate`, meaning every page load triggers a revalidation request. The 304 response still adds latency.
166+
167+
**Fix:** For static/versioned assets, use long-lived caching:
168+
169+
```
170+
cache-control: public, max-age=31536000, immutable
171+
```
172+
173+
Since this is deployed on Vercel, configure this in `next.config.js` or `vercel.json`.
174+
175+
#### 5. Network Dependency Chain (critical path: 1,123ms)
176+
177+
```
178+
HTML → CSS (a25ca756ef64d4e8.css, 717ms)
179+
└── Font (material-symbols-sharp.fcbbf02a.woff2, 786ms)
180+
```
181+
182+
Same font chain issue as the homepage — no `preconnect` or `preload` hints.
183+
184+
---
185+
186+
## Top Priorities (All Pages)
187+
188+
| Priority | Issue | Page(s) | Impact |
189+
| -------- | ------------------------------------------- | --------------- | ------------- |
190+
| 1 | CLS 0.25 from unsized footer logo | Admin Dashboard | CLS (Bad) |
191+
| 2 | Remove `loading="lazy"` from LCP image | Admin Dashboard | LCP |
192+
| 3 | Add `fetchpriority="high"` to LCP image | Admin Dashboard | LCP |
193+
| 4 | Preload `material-symbols-sharp.woff2` font | All pages | LCP / FCP |
194+
| 5 | Fix LCP text render delay (JS hydration) | Homepage | LCP |
195+
| 6 | Add `width`/`height` to carousel logos | Homepage | CLS stability |
196+
| 7 | Fix logo `cache-control` to long-lived | Admin Dashboard | Load speed |

src/app/api/cron/cycle-memberships/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export async function GET(request: NextRequest) {
3030
dbRes = await db
3131
.update(User)
3232
.set({ role: null, membership_expiry: null, reminder_pending: true })
33-
.where(and(eq(User.role, "member"), lte(User.membership_expiry, today)))
33+
.where(lte(User.membership_expiry, today))
3434
.returning()
3535
})
3636

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { sql } from "drizzle-orm"
2+
import type { NextRequest } from "next/server"
3+
4+
import { env } from "~/env"
5+
import { db } from "~/server/db"
6+
7+
export const dynamic = "force-dynamic"
8+
9+
export async function GET(request: NextRequest) {
10+
const authHeader = request.headers.get("authorization")
11+
if (authHeader !== `Bearer ${env.CRON_SECRET}`) {
12+
return new Response("Unauthorized", {
13+
status: 401,
14+
})
15+
}
16+
17+
let dbHealth
18+
try {
19+
const result = await db.execute<{ healthy: boolean }>(sql`SELECT true as healthy`)
20+
dbHealth = result.rows[0]?.healthy
21+
} catch (err) {
22+
dbHealth = false
23+
}
24+
25+
return Response.json(
26+
{
27+
db: dbHealth,
28+
},
29+
{ status: dbHealth ? 200 : 503 },
30+
)
31+
}

vercel.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
{
44
"path": "/api/cron/cycle-memberships",
55
"schedule": "59 15 31 12 *"
6+
},
7+
{
8+
"path": "/api/cron/prevent-supabase-pausing",
9+
"schedule": "0 0 */3 * *"
610
}
711
]
812
}

0 commit comments

Comments
 (0)