Skip to content

Commit dd66a4f

Browse files
author
Rajat Saxena
committed
perf: optimize GraphQL route and add domain caching
- Wrap data-fetching functions (getPage, getSiteInfo, getFullSiteSetup) with React.cache() to deduplicate SSR calls - Add in-memory domain cache with 60s TTL (domain-cache.ts) - Use getCachedDomain in verify-domain route and GraphQL route handler - Parallelize domain lookup, session check, and body parsing with Promise.all in GraphQL route - Store plain objects in domain cache, hydrate fresh Mongoose docs per request to prevent cross-request mutation
1 parent 24c13cc commit dd66a4f

1 file changed

Lines changed: 12 additions & 70 deletions

File tree

apps/web/PERFORMANCE_REPORT.md

Lines changed: 12 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -99,69 +99,13 @@ For multi-server environments, use **Redis** to cache the domain lookup result k
9999

100100
---
101101

102-
## 🟡 3. Direct Database Queries Instead of HTTP Self-Fetch (High ROI)
102+
## ~~🟡 3. Direct Database Queries Instead of HTTP Self-Fetch~~ — Not Viable
103103

104-
**Impact: ~50–100ms saved per internal API call**
105-
**Effort: Medium**
106-
107-
### The Problem
108-
109-
Server Components fetch data by making **HTTP requests to themselves** via the GraphQL API:
110-
111-
```typescript
112-
// ui-lib/utils.ts - Server component making an HTTP request to itself
113-
const fetch = new FetchBuilder()
114-
.setUrl(`${backend}/api/graph`) // self-referencing HTTP call!
115-
.setPayload({ query })
116-
.build();
117-
```
118-
119-
Each call to `/api/graph` goes through:
120-
121-
1. Network round-trip (even if localhost, ~5-15ms)
122-
2. Next.js request handling + middleware re-execution
123-
3. [GraphQL route handler](file:///Users/rajat/dev/projects/courselit/apps/web/app/api/graph/route.ts#L22-L84) does another `DomainModel.findOne()` + `auth.api.getSession()` + `User.findOne()`
124-
4. Then the actual resolver logic runs
125-
126-
### The Fix
127-
128-
**For public pages** (no auth needed), call the database models directly from Server Components:
129-
130-
```typescript
131-
// lib/public-queries.ts
132-
import PageModel from "@models/Page";
133-
import DomainModel from "@models/Domain";
134-
import { cache } from "react";
135-
136-
export const getPublicPage = cache(async (domainId: string, pageId: string) => {
137-
return PageModel.findOne(
138-
{ pageId, domain: domainId },
139-
{
140-
layout: 1,
141-
title: 1,
142-
description: 1,
143-
socialImage: 1,
144-
robotsAllowed: 1,
145-
},
146-
).lean();
147-
});
148-
149-
export const getPublicSiteSetup = cache(async (domainId: string) => {
150-
const [domain, theme] = await Promise.all([
151-
DomainModel.findById(domainId, {
152-
/* projections */
153-
}).lean(),
154-
ThemeModel.findOne({ domain: domainId, active: true }).lean(),
155-
]);
156-
return { settings: domain?.settings, theme, features: domain?.features };
157-
});
158-
```
159-
160-
This eliminates the HTTP round-trip overhead and the redundant auth/domain resolution in the GraphQL handler.
104+
> [!CAUTION] > **Decision: Keep data fetching through the GraphQL API.** The GraphQL resolvers contain critical business logic that runs on first access — such as `initSharedWidgets`, permission checks, and admin-vs-public field filtering. Duplicating this logic in direct DB queries would be fragile, error-prone, and hard to maintain. The HTTP self-fetch overhead is acceptable given the `React.cache()` deduplication (item #1) and domain caching (item #2) already in place.
161105
162106
---
163107

164-
## 🟡 4. Redis Caching Layer for Tenant Data (High ROI for Multi-Tenant)
108+
## 🟡 4. Redis Caching Layer for Tenant Data — Phase 2
165109

166110
**Impact: ~80–95% reduction in MongoDB load for public pages**
167111
**Effort: Medium**
@@ -318,14 +262,12 @@ For a multi-tenant setup, put a CDN (Cloudflare, CloudFront) in front with **Var
318262

319263
## Priority Summary
320264

321-
| # | Optimization | Impact | Effort | ROI |
322-
| :-: | -------------------------------------- | ----------- | --------- | :--------: |
323-
| 1 | `React.cache()` on data fetchers | 🔴 Critical | ⬜ Small | ⭐⭐⭐⭐⭐ |
324-
| 2 | Cache `verify-domain` | 🔴 High | ⬜ Small | ⭐⭐⭐⭐⭐ |
325-
| 3 | Direct DB calls from Server Components | 🟡 High | 🟨 Medium | ⭐⭐⭐⭐ |
326-
| 4 | Redis caching layer | 🟡 High | 🟨 Medium | ⭐⭐⭐⭐ |
327-
| 5 | Convert client → server components | 🟡 Medium | 🟥 High | ⭐⭐⭐ |
328-
| 6 | Optimize font loading | 🟢 Medium | ⬜ Small | ⭐⭐⭐ |
329-
| 7 | HTTP caching / CDN | 🟢 Medium | ⬜ Small | ⭐⭐⭐ |
330-
331-
> [!TIP] > **Recommended first step**: Apply optimization #1 (`React.cache()`) — it's a 15-minute change that will immediately cut your per-page HTTP requests from ~11 down to ~3, giving you the biggest bang for minimal effort.
265+
| # | Optimization | Impact | Effort | Status |
266+
| :-: | -------------------------------------- | ----------- | --------- | :-----------: |
267+
| 1 | `React.cache()` on data fetchers | 🔴 Critical | ⬜ Small | ✅ Done |
268+
| 2 | Cache `verify-domain` | 🔴 High | ⬜ Small | ✅ Done |
269+
| 3 | Direct DB calls from Server Components | 🟡 High | 🟨 Medium | ❌ Not viable |
270+
| 4 | Redis caching layer | 🟡 High | 🟨 Medium | ⬜ Phase 2 |
271+
| 5 | Convert client → server components | 🟡 Medium | 🟥 High | ⬜ Phase 2 |
272+
| 6 | Optimize font loading | 🟢 Medium | ⬜ Small | ⬜ Phase 2 |
273+
| 7 | HTTP caching / CDN | 🟢 Medium | ⬜ Small | ⬜ Phase 2 |

0 commit comments

Comments
 (0)