Skip to content

Commit c130661

Browse files
committed
add docs for caching and server timing
1 parent 64b016b commit c130661

File tree

7 files changed

+234
-38
lines changed

7 files changed

+234
-38
lines changed

app/root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const meta: V2_MetaFunction = () => {
7474

7575
export async function loader({ request }: DataFunctionArgs) {
7676
const cookieSession = await getSession(request.headers.get('Cookie'))
77-
const timings = makeTimings('rootLoader')
77+
const timings = makeTimings('root loader')
7878
const userId = await time(() => getUserId(request), {
7979
timings,
8080
type: 'getUserId',

app/routes/_marketing+/index.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1-
import type { HeadersFunction, V2_MetaFunction } from '@remix-run/node'
2-
import { logos, kodyRocket, stars } from './logos/logos.ts'
3-
import { combineServerTimings } from '~/utils/timing.server.ts'
1+
import type { V2_MetaFunction } from '@remix-run/node'
2+
import { kodyRocket, logos, stars } from './logos/logos.ts'
43

54
export const meta: V2_MetaFunction = () => [{ title: 'Epic Notes' }]
65

7-
export const headers: HeadersFunction = ({ loaderHeaders, parentHeaders }) => {
8-
const headers = {
9-
'Server-Timing': combineServerTimings(parentHeaders, loaderHeaders),
10-
}
11-
return headers
12-
}
13-
146
export default function Index() {
157
return (
168
<main className="relative min-h-screen sm:flex sm:items-center sm:justify-center">

app/routes/users+/$username_+/notes.$noteId_.edit.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { json, type DataFunctionArgs } from '@remix-run/node'
22
import { useLoaderData } from '@remix-run/react'
33
import { NoteEditor } from '~/routes/resources+/note-editor.tsx'
4+
import { requireUserId } from '~/utils/auth.server.ts'
45
import { prisma } from '~/utils/db.server.ts'
56

6-
export async function loader({ params }: DataFunctionArgs) {
7-
const note = await prisma.note.findUnique({
7+
export async function loader({ params, request }: DataFunctionArgs) {
8+
const userId = await requireUserId(request)
9+
const note = await prisma.note.findFirst({
810
where: {
911
id: params.noteId,
12+
ownerId: userId,
1013
},
1114
})
1215
if (!note) {

app/routes/users+/$username_+/notes.new.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1+
import { json } from '@remix-run/router'
2+
import { type DataFunctionArgs } from '@remix-run/server-runtime'
13
import { NoteEditor } from '~/routes/resources+/note-editor.tsx'
4+
import { requireUserId } from '~/utils/auth.server.ts'
5+
6+
export async function loader({ request }: DataFunctionArgs) {
7+
await requireUserId(request)
8+
return json({})
9+
}
210

311
export default function NewNoteRoute() {
412
return <NoteEditor />

app/routes/users+/$username_+/notes.tsx

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,62 @@
1-
import { json, type DataFunctionArgs } from '@remix-run/node'
1+
import {
2+
json,
3+
type DataFunctionArgs,
4+
type HeadersFunction,
5+
} from '@remix-run/node'
26
import { Link, NavLink, Outlet, useLoaderData } from '@remix-run/react'
37
import { twMerge } from 'tailwind-merge'
48
import { GeneralErrorBoundary } from '~/components/error-boundary.tsx'
5-
import { requireUserId } from '~/utils/auth.server.ts'
69
import { prisma } from '~/utils/db.server.ts'
710
import { getUserImgSrc } from '~/utils/misc.ts'
11+
import {
12+
combineServerTimings,
13+
makeTimings,
14+
time,
15+
} from '~/utils/timing.server.ts'
816

9-
export async function loader({ params, request }: DataFunctionArgs) {
10-
await requireUserId(request, { redirectTo: null })
11-
const owner = await prisma.user.findUnique({
12-
where: {
13-
username: params.username,
14-
},
15-
select: {
16-
id: true,
17-
username: true,
18-
name: true,
19-
imageId: true,
20-
},
21-
})
17+
export async function loader({ params }: DataFunctionArgs) {
18+
const timings = makeTimings('notes loader')
19+
const owner = await time(
20+
() =>
21+
prisma.user.findUnique({
22+
where: {
23+
username: params.username,
24+
},
25+
select: {
26+
id: true,
27+
username: true,
28+
name: true,
29+
imageId: true,
30+
},
31+
}),
32+
{ timings, type: 'find user' },
33+
)
2234
if (!owner) {
2335
throw new Response('Not found', { status: 404 })
2436
}
25-
const notes = await prisma.note.findMany({
26-
where: {
27-
ownerId: owner.id,
28-
},
29-
select: {
30-
id: true,
31-
title: true,
32-
},
33-
})
34-
return json({ owner, notes })
37+
const notes = await time(
38+
() =>
39+
prisma.note.findMany({
40+
where: {
41+
ownerId: owner.id,
42+
},
43+
select: {
44+
id: true,
45+
title: true,
46+
},
47+
}),
48+
{ timings, type: 'find notes' },
49+
)
50+
return json(
51+
{ owner, notes },
52+
{ headers: { 'Server-Timing': timings.toString() } },
53+
)
54+
}
55+
56+
export const headers: HeadersFunction = ({ loaderHeaders, parentHeaders }) => {
57+
return {
58+
'Server-Timing': combineServerTimings(parentHeaders, loaderHeaders),
59+
}
3560
}
3661

3762
export default function NotesRoute() {

docs/caching.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Caching
2+
3+
The Epic Stack comes with caching utilities and a management dashboard that
4+
allows you to view and clear your cache. There are two caches built into the
5+
Epic Stack:
6+
7+
- **SQLite**: This is a separate database from the main application database.
8+
It's managed by LiteFS so the data is replicated across all instances of your
9+
app. This can be used for long-lived cached values.
10+
- **LRU**: This is an in-memory cache that is used to store the results of
11+
expensive queries or help deduplicate requests for data. It's not replicated
12+
across instances and as it's in-memory it will be cleared when your app is
13+
restarted. So this should be used for short-lived cached values.
14+
15+
Caching is intended to be used for data that is expensive and/or slow to compute
16+
or retrieve. It can help you avoid costs or rate limits associated with making
17+
requests to third parties.
18+
19+
It's important to note that caching should not be the first solution to slowness
20+
issues. If you've got a slow query, look into optimizing it with database
21+
indexes before caching the results.
22+
23+
## Using the cache
24+
25+
You won't typically interact directly with the caches. Instead, you will use
26+
[`cachified`](https://npm.im/cachified) which is a nice abstraction for cache
27+
management. We have a small abstraction on top of it which allows you to pass
28+
`timings` to work seamlessly with
29+
[the server timing utility](./server-timing.md).
30+
31+
Let's say we're making a request to tito to get a list of events. Tito's API is
32+
kinda slow and our event details don't change much so we're ok speeding things
33+
up by caching them and utilizing the stale-while-revalidate features in
34+
cachified. Here's how you would use cachified to do this:
35+
36+
```tsx
37+
import { cachified, cache } from '~/utils/cache.server.ts'
38+
import { type Timings } from '~/utils/timing.server.ts'
39+
40+
const eventSchema = z.object({
41+
/* the schema for events */
42+
})
43+
44+
export async function getScheduledEvents({
45+
timings,
46+
}: {
47+
timings: Timings
48+
} = {}) {
49+
const scheduledEvents = await cachified({
50+
key: 'tito:scheduled-events',
51+
cache,
52+
timings,
53+
getFreshValue: () => {
54+
// do a fetch request to the tito API and stuff here
55+
return [
56+
/* the events you got from tito */
57+
]
58+
},
59+
checkValue: eventSchema.array(),
60+
// Time To Live (ttl): the cached value is considered valid for 24 hours
61+
ttl: 1000 * 60 * 24,
62+
// Stale While Revalidate (swr): if the cached value is less than 30 days
63+
// expired, return it while fetching a fresh value in the background
64+
staleWhileRevalidate: 1000 * 60 * 60 * 24 * 30,
65+
})
66+
return scheduledEvents
67+
}
68+
```
69+
70+
With this setup, the first time you call `getScheduledEvents` it will make a
71+
request to the tito API and return the results. It will also cache the results
72+
in the `cache` (which is the SQLite cache). The next time you call
73+
`getScheduledEvents` it will return the cached value if the cached value is less
74+
than 30 days old. If the cached value is older than 24 hours, it will also make
75+
a request to the tito API in the. If the cache value is more than 30 days old,
76+
it will wait until the tito request is complete and then return the fresh value.
77+
78+
Bottom line: You make the request much less often and users are never waiting
79+
for it.
80+
81+
A lot more needs to be said on this subject (an entire workshop full!), but this
82+
should be enough to get you going!

docs/server-timing.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Server Timing
2+
3+
![Network tab of Chrome DevTools showing the Timing tab of a specific network call and an arrow pointing to the Server Timing section with the words "This is what server timings do"](https://github.com/epicweb-dev/epic-stack/assets/1500684/e5a28253-8204-43b1-8222-3f287d024ca5)
4+
5+
The Epic Stack comes with a built-in server timing utility that allows you to
6+
measure the performance of your application. You can find it in the
7+
`app/utils/timing.server.ts` file. The idea is you can wrap a function in a
8+
`time` call and then use the timings object to generate a `Server-Timing` header
9+
which you can then use to have fine grained timing metrics for requests made in
10+
your app.
11+
12+
You can
13+
[learn more about the Server Timing header on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing).
14+
The metrics passed in this header will be visually displayed in
15+
[the DevTools "Timing" tab](https://developer.chrome.com/docs/devtools/network/reference/#timing).
16+
17+
## Usage
18+
19+
Timings requires four parts:
20+
21+
1. Setup Timings
22+
2. Time functions
23+
3. Create headers
24+
4. Send headers
25+
26+
Here are all those parts in action in the `/user/:username/notes` route at the
27+
time of this writing:
28+
29+
```tsx
30+
import {
31+
combineServerTimings,
32+
makeTimings,
33+
time,
34+
} from '~/utils/timing.server.ts'
35+
36+
export async function loader({ params }: DataFunctionArgs) {
37+
const timings = makeTimings('notes loader') // <-- 1. Setup Timings
38+
// 2. Time functions
39+
const owner = await time(
40+
() =>
41+
prisma.user.findUnique({
42+
where: {
43+
username: params.username,
44+
},
45+
select: {
46+
id: true,
47+
username: true,
48+
name: true,
49+
imageId: true,
50+
},
51+
}),
52+
{ timings, type: 'find user' },
53+
)
54+
if (!owner) {
55+
throw new Response('Not found', { status: 404 })
56+
}
57+
// 2. Time functions
58+
const notes = await time(
59+
() =>
60+
prisma.note.findMany({
61+
where: {
62+
ownerId: owner.id,
63+
},
64+
select: {
65+
id: true,
66+
title: true,
67+
},
68+
}),
69+
{ timings, type: 'find notes' },
70+
)
71+
return json(
72+
{ owner, notes },
73+
{ headers: { 'Server-Timing': timings.toString() } }, // <-- 3. Create headers
74+
)
75+
}
76+
77+
export const headers: HeadersFunction = ({ loaderHeaders, parentHeaders }) => {
78+
return {
79+
'Server-Timing': combineServerTimings(parentHeaders, loaderHeaders), // <-- 4. Send headers
80+
}
81+
}
82+
```
83+
84+
You can
85+
[learn more about `headers` in the Remix docs](https://remix.run/docs/en/main/route/headers)
86+
(note, the Epic Stack has the v2 behavior enabled).

0 commit comments

Comments
 (0)