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 });
}
九、总结 #
本章我们学习了:
- 嵌套路由并行:自动并行加载所有层级数据
- Promise.all:在单路由中并行加载多个数据
- 延迟加载:使用 defer 和 Await 处理非关键数据
- 数据依赖:处理有依赖关系的数据加载
- 性能优化:预取、缓存和批量请求
核心要点:
- 利用嵌套路由实现自动并行加载
- 使用 Promise.all 并行加载独立数据
- 使用 defer 延迟加载非关键数据
- 合理划分路由以最大化并行效果
最后更新:2026-03-28