Remix缓存策略 #
一、缓存概述 #
Remix 提供了多层缓存机制,包括 HTTP 缓存、服务端缓存和客户端缓存。合理使用缓存可以显著提升应用性能。
二、HTTP缓存 #
2.1 Cache-Control头 #
在 loader 中设置 HTTP 缓存:
tsx
import { json } from "@remix-run/node";
export async function loader() {
const data = await fetchData();
return json(data, {
headers: {
"Cache-Control": "public, max-age=60, s-maxage=300",
},
});
}
2.2 缓存指令说明 #
| 指令 | 说明 |
|---|---|
public |
可被任何缓存存储 |
private |
只能被浏览器缓存 |
max-age |
浏览器缓存时间(秒) |
s-maxage |
CDN缓存时间(秒) |
no-cache |
使用前需验证 |
no-store |
不缓存 |
must-revalidate |
过期后必须验证 |
2.3 不同场景的缓存策略 #
静态内容(长时间缓存):
tsx
export async function loader() {
return json(data, {
headers: {
"Cache-Control": "public, max-age=31536000, immutable",
},
});
}
动态内容(短时间缓存):
tsx
export async function loader() {
return json(data, {
headers: {
"Cache-Control": "public, max-age=60, stale-while-revalidate=300",
},
});
}
用户相关内容(私有缓存):
tsx
export async function loader() {
return json(data, {
headers: {
"Cache-Control": "private, max-age=60",
},
});
}
实时数据(不缓存):
tsx
export async function loader() {
return json(data, {
headers: {
"Cache-Control": "no-store",
},
});
}
三、客户端缓存 #
3.1 Remix内置缓存 #
Remix 自动缓存 loader 数据,导航时会复用缓存:
tsx
// 第一次访问时加载数据
// 返回时使用缓存,不会重新加载
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.id);
return json({ post });
}
3.2 强制重新验证 #
使用 shouldRevalidate 控制缓存:
tsx
import type { ShouldRevalidateFunction } from "@remix-run/react";
export const shouldRevalidate: ShouldRevalidateFunction = ({
actionResult,
defaultShouldRevalidate,
}) => {
// 只在特定操作后重新验证
if (actionResult?.updated) {
return true;
}
return false;
};
3.3 使用useRevalidator #
手动触发重新验证:
tsx
import { useRevalidator } from "@remix-run/react";
export default function Page() {
const revalidator = useRevalidator();
const handleRefresh = () => {
revalidator.revalidate();
};
return (
<div>
<button onClick={handleRefresh}>刷新</button>
{/* 内容 */}
</div>
);
}
四、数据重新验证 #
4.1 Action后自动重新验证 #
Action 执行后,Remix 自动重新加载所有 loader:
tsx
// routes/posts._index.tsx
export async function loader() {
const posts = await getPosts();
return json({ posts });
}
// routes/posts.new.tsx
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
await createPost(formData);
return redirect("/posts"); // 重定向后自动重新加载loader
}
4.2 控制重新验证范围 #
tsx
export const shouldRevalidate: ShouldRevalidateFunction = ({
currentUrl,
nextUrl,
defaultShouldRevalidate,
}) => {
// 只在特定路径变化时重新验证
if (currentUrl.pathname !== nextUrl.pathname) {
return true;
}
return false;
};
4.3 使用useNavigation监听 #
tsx
import { useNavigation, useRevalidator } from "@remix-run/react";
export default function Page() {
const navigation = useNavigation();
const revalidator = useRevalidator();
useEffect(() => {
if (navigation.state === "idle" && shouldRefresh) {
revalidator.revalidate();
}
}, [navigation.state]);
return <div>{/* 内容 */}</div>;
}
五、服务端缓存 #
5.1 内存缓存 #
tsx
const cache = new Map<string, { data: any; expires: number }>();
export async function loader({ params }: LoaderFunctionArgs) {
const cacheKey = `post-${params.id}`;
const cached = cache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return json(cached.data);
}
const post = await getPost(params.id);
cache.set(cacheKey, {
data: post,
expires: Date.now() + 60 * 1000, // 1分钟
});
return json({ post });
}
5.2 使用Redis缓存 #
tsx
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
export async function loader({ params }: LoaderFunctionArgs) {
const cacheKey = `post:${params.id}`;
const cached = await redis.get(cacheKey);
if (cached) {
return json(JSON.parse(cached));
}
const post = await getPost(params.id);
await redis.setex(cacheKey, 60, JSON.stringify(post));
return json({ post });
}
5.3 缓存装饰器 #
tsx
function withCache<T>(
key: string,
ttl: number,
fn: () => Promise<T>
): Promise<T> {
return async () => {
const cached = await cache.get(key);
if (cached) return cached;
const data = await fn();
await cache.set(key, data, ttl);
return data;
};
}
export async function loader({ params }: LoaderFunctionArgs) {
const post = await withCache(
`post:${params.id}`,
60,
() => getPost(params.id)
);
return json({ post });
}
六、ETag和Last-Modified #
6.1 使用ETag #
tsx
import { createHash } from "crypto";
export async function loader({ request }: LoaderFunctionArgs) {
const data = await fetchData();
const etag = createHash("md5")
.update(JSON.stringify(data))
.digest("hex");
if (request.headers.get("If-None-Match") === etag) {
return new Response(null, { status: 304 });
}
return json(data, {
headers: { "ETag": etag },
});
}
6.2 使用Last-Modified #
tsx
export async function loader({ request }: LoaderFunctionArgs) {
const data = await fetchData();
const lastModified = new Date(data.updatedAt).toUTCString();
if (request.headers.get("If-Modified-Since") === lastModified) {
return new Response(null, { status: 304 });
}
return json(data, {
headers: { "Last-Modified": lastModified },
});
}
七、缓存失效策略 #
7.1 主动失效 #
tsx
const cache = new Map<string, any>();
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const post = await updatePost(formData);
// 主动清除缓存
cache.delete(`post:${post.id}`);
cache.delete("posts:list");
return redirect(`/posts/${post.id}`);
}
7.2 基于时间的失效 #
tsx
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
function createCache() {
const cache = new Map<string, CacheEntry<any>>();
return {
get<T>(key: string): T | null {
const entry = cache.get(key);
if (!entry) return null;
if (Date.now() - entry.timestamp > entry.ttl) {
cache.delete(key);
return null;
}
return entry.data;
},
set<T>(key: string, data: T, ttl: number) {
cache.set(key, {
data,
timestamp: Date.now(),
ttl,
});
},
invalidate(pattern: RegExp) {
for (const key of cache.keys()) {
if (pattern.test(key)) {
cache.delete(key);
}
}
},
};
}
八、最佳实践 #
8.1 分层缓存 #
tsx
export async function loader({ params, request }: LoaderFunctionArgs) {
// 1. 检查HTTP缓存
const ifNoneMatch = request.headers.get("If-None-Match");
// 2. 检查服务端缓存
const cached = await cache.get(`post:${params.id}`);
if (cached) {
const etag = generateETag(cached);
if (ifNoneMatch === etag) {
return new Response(null, { status: 304 });
}
return json(cached, {
headers: {
"ETag": etag,
"Cache-Control": "public, max-age=60",
},
});
}
// 3. 从数据库获取
const post = await getPost(params.id);
await cache.set(`post:${params.id}`, post, 60);
const etag = generateETag(post);
return json(post, {
headers: {
"ETag": etag,
"Cache-Control": "public, max-age=60",
},
});
}
8.2 合理设置缓存时间 #
tsx
// 静态资源:长缓存
const STATIC_CACHE = "public, max-age=31536000, immutable";
// API数据:短缓存
const API_CACHE = "public, max-age=60, stale-while-revalidate=300";
// 用户数据:私有缓存
const USER_CACHE = "private, max-age=60";
// 实时数据:不缓存
const NO_CACHE = "no-store";
8.3 缓存键设计 #
tsx
function createCacheKey(
resource: string,
params: Record<string, string>,
userId?: string
) {
const parts = [resource];
for (const [key, value] of Object.entries(params)) {
parts.push(`${key}:${value}`);
}
if (userId) {
parts.push(`user:${userId}`);
}
return parts.join(":");
}
九、总结 #
本章我们学习了:
- HTTP缓存:Cache-Control 和缓存指令
- 客户端缓存:Remix 内置缓存机制
- 数据重新验证:shouldRevalidate 和 useRevalidator
- 服务端缓存:内存缓存和 Redis
- ETag和Last-Modified:条件请求
核心要点:
- 根据数据特性选择合适的缓存策略
- 使用多层缓存提升性能
- 合理设置缓存失效时间
- 主动失效相关缓存
最后更新:2026-03-28