async-api-routes.md
| title | impact | impactDescription | tags |
|---|---|---|---|
| API Routes에서 Waterfall Chains 방지 | CRITICAL | 2-10배 개선 | api-routes, server-actions, waterfalls, parallelization |
API Routes에서 Waterfall Chains 방지
API routes와 Server Actions에서 독립적인 작업은 아직 await하지 않더라도 즉시 시작하세요.
Incorrect (config가 auth를 기다리고, data가 둘 다 기다림):
export async function GET(request: Request) {
const session = await auth();
const config = await fetchConfig();
const data = await fetchData(session.user.id);
return Response.json({ data, config });
}Correct (auth와 config가 즉시 시작됨):
export async function GET(request: Request) {
const sessionPromise = auth();
const configPromise = fetchConfig();
const session = await sessionPromise;
const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]);
return Response.json({ data, config });
}더 복잡한 dependency chains이 있는 작업의 경우, better-all을 사용하여 자동으로 병렬성을 최대화하세요 (Dependency-Based Parallelization 참조).
async-defer-await.md
| title | impact | impactDescription | tags |
|---|---|---|---|
| 필요할 때까지 Await 지연 | HIGH | 사용되지 않는 코드 경로 blocking 방지 | async, await, conditional, optimization |
필요할 때까지 Await 지연
await 작업을 실제로 사용되는 분기로 이동하여 필요하지 않은 코드 경로를 blocking하는 것을 피하세요.
Incorrect (두 분기 모두 block됨):
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId);
if (skipProcessing) {
// 즉시 반환하지만 여전히 userData를 기다림
return { skipped: true };
}
// 이 분기만 userData를 사용함
return processUserData(userData);
}Correct (필요할 때만 block됨):
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// 기다리지 않고 즉시 반환
return { skipped: true };
}
// 필요할 때만 fetch
const userData = await fetchUserData(userId);
return processUserData(userData);
}또 다른 예시 (early return 최적화):
// Incorrect: 항상 permissions를 fetch함
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId);
const resource = await getResource(resourceId);
if (!resource) {
return { error: "Not found" };
}
if (!permissions.canEdit) {
return { error: "Forbidden" };
}
return await updateResourceData(resource, permissions);
}
// Correct: 필요할 때만 fetch함
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId);
if (!resource) {
return { error: "Not found" };
}
const permissions = await fetchPermissions(userId);
if (!permissions.canEdit) {
return { error: "Forbidden" };
}
return await updateResourceData(resource, permissions);
}이 최적화는 skip되는 분기가 자주 실행되거나, 지연된 작업이 비용이 클 때 특히 가치가 있습니다.
async-dependencies.md
| title | impact | impactDescription | tags |
|---|---|---|---|
| Dependency 기반 병렬화 | CRITICAL | 2-10배 개선 | async, parallelization, dependencies, better-all |
Dependency 기반 병렬화
부분적인 dependencies가 있는 작업의 경우, better-all을 사용하여 병렬성을 최대화하세요. 각 task를 가능한 가장 빠른 시점에 자동으로 시작합니다.
Incorrect (profile이 config를 불필요하게 기다림):
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
const profile = await fetchProfile(user.id);Correct (config와 profile이 병렬로 실행됨):
import { all } from "better-all";
const { user, config, profile } = await all({
async user() {
return fetchUser();
},
async config() {
return fetchConfig();
},
async profile() {
return fetchProfile((await this.$.user).id);
},
});Reference: https://github.com/shuding/better-all
async-parallel.md
| title | impact | impactDescription | tags |
|---|---|---|---|
| 독립적인 작업을 위한 Promise.all() | CRITICAL | 2-10배 개선 | async, parallelization, promises, waterfalls |
독립적인 작업을 위한 Promise.all()
async 작업들이 서로 의존성이 없을 때, Promise.all()을 사용하여 동시에 실행하세요.
Incorrect (순차 실행, 3번의 round trips):
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();Correct (병렬 실행, 1번의 round trip):
const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]);async-suspense-boundaries.md
| title | impact | impactDescription | tags |
|---|---|---|---|
| 전략적 Suspense Boundaries | HIGH | 더 빠른 initial paint | async, suspense, streaming, layout-shift |
전략적 Suspense Boundaries
async components에서 JSX를 반환하기 전에 데이터를 await하는 대신, Suspense boundaries를 사용하여 데이터가 로드되는 동안 wrapper UI를 더 빠르게 보여주세요.
Incorrect (wrapper가 data fetching에 의해 block됨):
async function Page() {
const data = await fetchData(); // 전체 페이지를 block함
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<DataDisplay data={data} />
</div>
<div>Footer</div>
</div>
);
}중간 섹션만 데이터가 필요한데도 전체 레이아웃이 데이터를 기다립니다.
Correct (wrapper가 즉시 표시되고, data는 stream됨):
function Page() {
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
</div>
<div>Footer</div>
</div>
);
}
async function DataDisplay() {
const data = await fetchData(); // 이 component만 block함
return <div>{data.content}</div>;
}Sidebar, Header, Footer는 즉시 렌더링됩니다. DataDisplay만 데이터를 기다립니다.
Alternative (components 간 promise 공유):
function Page() {
// fetch를 즉시 시작하지만 await하지 않음
const dataPromise = fetchData();
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<Suspense fallback={<Skeleton />}>
<DataDisplay dataPromise={dataPromise} />
<DataSummary dataPromise={dataPromise} />
</Suspense>
<div>Footer</div>
</div>
);
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise); // promise를 unwrap함
return <div>{data.content}</div>;
}
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise); // 동일한 promise를 재사용
return <div>{data.summary}</div>;
}두 components가 같은 promise를 공유하므로 fetch는 한 번만 발생합니다. 레이아웃은 즉시 렌더링되고 두 components는 함께 기다립니다.
이 패턴을 사용하지 말아야 할 때:
- 레이아웃 결정에 필요한 critical data (위치에 영향을 줌)
- above the fold의 SEO-critical 콘텐츠
- suspense 오버헤드가 가치 없는 작고 빠른 쿼리
- layout shift를 피하고 싶을 때 (loading → content jump)
Trade-off: 더 빠른 initial paint vs 잠재적 layout shift. UX 우선순위에 따라 선택하세요.