Remix并行数据加载 #

一、并行加载概述 #

Remix 的核心优势之一是并行数据加载。当访问嵌套路由时,所有层级的 loader 会并行执行,显著减少页面加载时间。

二、嵌套路由并行加载 #

2.1 自动并行加载 #

访问嵌套路由时,所有 loader 并行执行:

text
访问 /admin/users
├── root.tsx loader      ─┐
├── admin.tsx loader     ─┼─ 并行执行
└── admin.users.tsx loader ─┘
tsx
// app/root.tsx
export async function loader() {
  const settings = await getSettings();
  return json({ settings });
}

// app/routes/admin.tsx
export async function loader({ request }: LoaderFunctionArgs) {
  const user = await getUser(request);
  const notifications = await getNotifications(user.id);
  return json({ user, notifications });
}

// app/routes/admin.users.tsx
export async function loader() {
  const users = await getUsers();
  return json({ users });
}

2.2 加载时间对比 #

传统方式(串行):

text
root loader:    ████████ (200ms)
admin loader:           ████████ (200ms)
users loader:                   ████████ (200ms)
总时间: 600ms

Remix方式(并行):

text
root loader:    ████████ (200ms)
admin loader:   ████████ (200ms)
users loader:   ████████ (200ms)
总时间: 200ms

三、单路由并行加载 #

3.1 使用Promise.all #

在单个 loader 中并行加载多个数据:

tsx
export async function loader({ params }: LoaderFunctionArgs) {
  const [post, comments, author, relatedPosts] = await Promise.all([
    getPost(params.id),
    getComments(params.id),
    getAuthor(params.postId),
    getRelatedPosts(params.id),
  ]);
  
  return json({ post, comments, author, relatedPosts });
}

3.2 错误处理 #

tsx
export async function loader({ params }: LoaderFunctionArgs) {
  const [post, comments, author] = await Promise.allSettled([
    getPost(params.id),
    getComments(params.id),
    getAuthor(params.postId),
  ]);
  
  // 处理各个结果
  if (post.status === "rejected") {
    throw new Response("文章不存在", { status: 404 });
  }
  
  return json({
    post: post.value,
    comments: comments.status === "fulfilled" ? comments.value : [],
    author: author.status === "fulfilled" ? author.value : null,
  });
}

3.3 条件并行加载 #

tsx
export async function loader({ request, params }: LoaderFunctionArgs) {
  const url = new URL(request.url);
  const includeComments = url.searchParams.get("comments") === "true";
  
  const promises: Promise<any>[] = [
    getPost(params.id),
  ];
  
  if (includeComments) {
    promises.push(getComments(params.id));
  }
  
  const [post, comments] = await Promise.all(promises);
  
  return json({
    post,
    comments: includeComments ? comments : null,
  });
}

四、延迟加载 #

4.1 使用defer #

对于非关键数据,可以使用延迟加载:

tsx
import { defer } from "@remix-run/node";
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.id);  // 关键数据,立即加载
  const comments = getComments(params.id);  // 非关键数据,延迟加载
  
  return defer({ post, comments });
}

export default function Post() {
  const { post, comments } = useLoaderData<typeof loader>();
  
  return (
    <div>
      <article>{post.content}</article>
      
      <Suspense fallback={<div>加载评论中...</div>}>
        <Await resolve={comments}>
          {(comments) => (
            <ul>
              {comments.map((comment) => (
                <li key={comment.id}>{comment.content}</li>
              ))}
            </ul>
          )}
        </Await>
      </Suspense>
    </div>
  );
}

4.2 错误处理 #

tsx
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";

export default function Post() {
  const { post, comments } = useLoaderData<typeof loader>();
  
  return (
    <div>
      <article>{post.content}</article>
      
      <Suspense fallback={<div>加载评论中...</div>}>
        <Await
          resolve={comments}
          errorElement={<div>评论加载失败</div>}
        >
          {(comments) => (
            <ul>
              {comments.map((comment) => (
                <li key={comment.id}>{comment.content}</li>
              ))}
            </ul>
          )}
        </Await>
      </Suspense>
    </div>
  );
}

五、数据依赖处理 #

5.1 串行加载(有依赖) #

当数据之间有依赖关系时:

tsx
export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.id);
  const author = await getAuthor(post.authorId);
  const authorPosts = await getAuthorPosts(author.id);
  
  return json({ post, author, authorPosts });
}

5.2 部分并行 #

tsx
export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.id);
  
  // 依赖post的数据并行加载
  const [author, comments, relatedPosts] = await Promise.all([
    getAuthor(post.authorId),
    getComments(post.id),
    getRelatedPosts(post.id),
  ]);
  
  return json({ post, author, comments, relatedPosts });
}

六、性能优化技巧 #

6.1 数据预取 #

tsx
import { Link } from "@remix-run/react";

export default function PostList({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link
            to={`/posts/${post.id}`}
            prefetch="intent"
          >
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  );
}

6.2 避免重复请求 #

tsx
const postCache = new Map<string, Promise<Post>>();

async function getPostCached(id: string): Promise<Post> {
  if (postCache.has(id)) {
    return postCache.get(id)!;
  }
  
  const promise = getPost(id);
  postCache.set(id, promise);
  
  return promise;
}

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPostCached(params.id);
  return json({ post });
}

6.3 批量请求 #

tsx
export async function loader({ request }: LoaderFunctionArgs) {
  const url = new URL(request.url);
  const ids = url.searchParams.getAll("id");
  
  // 批量获取,减少请求次数
  const posts = await getPostsByIds(ids);
  
  return json({ posts });
}

七、加载状态显示 #

7.1 全局加载状态 #

tsx
import { useNavigation } from "@remix-run/react";

export default function Page() {
  const navigation = useNavigation();
  
  const isLoading = navigation.state === "loading";
  
  return (
    <div>
      {isLoading && (
        <div className="fixed top-0 left-0 right-0 h-1 bg-blue-500 animate-pulse" />
      )}
      {/* 内容 */}
    </div>
  );
}

7.2 局部加载状态 #

tsx
import { useNavigation } from "@remix-run/react";

export default function Comments() {
  const navigation = useNavigation();
  
  const isNavigatingToComments = 
    navigation.location?.pathname.includes("/comments");
  
  return (
    <div className={isNavigatingToComments ? "opacity-50" : ""}>
      {/* 评论列表 */}
    </div>
  );
}

八、最佳实践 #

8.1 合理划分路由 #

text
推荐:
app/routes/
├── _index.tsx
├── posts._index.tsx      # 独立加载文章列表
├── posts.$id.tsx         # 独立加载文章详情
└── admin.tsx             # 共享管理布局
    └── admin.users.tsx   # 独立加载用户列表

不推荐:
app/routes/
├── _index.tsx
└── posts.tsx             # 在这里加载所有文章数据
    └── posts.$id.tsx     # 又重复加载文章数据

8.2 数据分层 #

tsx
// 关键数据:立即加载
const post = await getPost(params.id);

// 重要数据:并行加载
const [author, comments] = await Promise.all([
  getAuthor(post.authorId),
  getComments(post.id),
]);

// 非关键数据:延迟加载
const recommendations = getRecommendations(post.id);

return defer({ post, author, comments, recommendations });

8.3 错误隔离 #

tsx
export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.id);
  
  // 即使评论加载失败,文章仍然可以显示
  const comments = await getComments(params.id).catch(() => []);
  
  return json({ post, comments });
}

九、总结 #

本章我们学习了:

  1. 嵌套路由并行:自动并行加载所有层级数据
  2. Promise.all:在单路由中并行加载多个数据
  3. 延迟加载:使用 defer 和 Await 处理非关键数据
  4. 数据依赖:处理有依赖关系的数据加载
  5. 性能优化:预取、缓存和批量请求

核心要点:

  • 利用嵌套路由实现自动并行加载
  • 使用 Promise.all 并行加载独立数据
  • 使用 defer 延迟加载非关键数据
  • 合理划分路由以最大化并行效果
最后更新:2026-03-28