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(":");
}

九、总结 #

本章我们学习了:

  1. HTTP缓存:Cache-Control 和缓存指令
  2. 客户端缓存:Remix 内置缓存机制
  3. 数据重新验证:shouldRevalidate 和 useRevalidator
  4. 服务端缓存:内存缓存和 Redis
  5. ETag和Last-Modified:条件请求

核心要点:

  • 根据数据特性选择合适的缓存策略
  • 使用多层缓存提升性能
  • 合理设置缓存失效时间
  • 主动失效相关缓存
最后更新:2026-03-28