기록

Vercel Region과 RSC 프리페치 지연

TL;DR

  • 서울 사용자의 요청이 Vercel Seoul Edge에서 Washington Function까지 왕복하고 있었다.
  • Function 실행 시간은 7ms였지만 응답 완료까지는 228ms가 걸렸다.
  • Function Region을 Seoul로 옮긴 뒤 같은 요청은 43ms에 끝났다.
  • 단일 요청 차이 185ms는 RSC 요청과 segment prefetch가 겹치면 화면 완성 지연으로 더 크게 보인다.
  • Region은 왕복 1건의 비용을 줄이고, Link/prefetch/cache 구조는 왕복 횟수를 줄인다.

운영 배포 사이트에서 화면 전환이 느렸다. Vercel 로그를 보면 서버 코드보다 Region 왕복이 먼저 보였다.

Received in Seoul, South Korea (icn1)
Routed to Washington, D.C., USA (iad1)

Function Invocation Execution Duration: 7ms External APIs No outgoing requests

Response finished in 228ms

요청은 서울 Edge에서 받았지만 Function은 Washington Region에서 실행됐다. Function 자체는 7ms만 실행됐고 외부 API도 없었다. 나머지 시간은 사용자의 위치와 Function Region 사이의 왕복 비용으로 봐야 했다.

Region 변경 전후

Vercel Function Region에 Seoul(icn1)을 추가했다. 백엔드와 DB도 한국 Region에 있었기 때문에, Function만 서울로 옮겨도 데이터 소스와 멀어지는 문제는 없었다. 변경 후 같은 종류의 요청 로그는 다음과 같았다.

Received in Seoul, South Korea (icn1)

Function Invocation Execution Duration: 23ms External APIs No outgoing requests

Response finished in 43ms
항목 변경 전 변경 후
Edge Region Seoul (icn1) Seoul (icn1)
Function Region Washington (iad1) Seoul (icn1)
Function Execution 7ms 23ms
Response Finished 228ms 43ms
차이 185ms 감소

Function 실행 시간은 7ms에서 23ms로 늘었지만, 사용자가 기다리는 전체 응답 시간은 228ms에서 43ms로 줄었다. 화면에서 중요한 값은 Function 내부 실행 시간이 아니라 응답이 끝나는 시간이다.

요청 하나의 차이와 화면의 차이

단일 요청 차이는 185ms다.

228ms - 43ms = 185ms

하지만 App Router 화면 전환은 요청 하나로 끝나지 않는다. RSC 요청, segment prefetch, 라우트별 데이터 요청이 함께 발생한다. Playwright 측정에서는 Washington 왕복을 오리진 Function 요청에만 185ms 지연으로 주입했다. 정적 에셋이나 Edge에서 끝나는 요청에는 지연을 넣지 않았다.

await context.route("**://<deployment-domain>/**", async (route) => {
  const response = await route.fetch();
  const headers = response.headers();

const originBound = (headers["x-vercel-id"] ?? "").split("::").length >= 3 || headers["x-vercel-cache"] === "MISS"; if (originBound) { await new Promise((resolve) => setTimeout(resolve, 185)); }

await route.fulfill({ response });
});

측정 결과는 다음과 같았다.

지표 서울 icn1 워싱턴 기준 차이
진입 FCP 176ms 284ms +108ms
진입 LCP 612ms 720ms +108ms
진입 화면 완성 1,556ms 2,512ms +956ms
탭 전환 click→URL 141ms 313ms +172ms
탭 전환 click→화면 완성 850ms 1,040ms +190ms
정적 화면 진입 오리진 왕복 0건 동일 0

워크스페이스 진입 화면에서는 단일 요청 185ms 차이가 화면 완성 기준 약 956ms 차이로 나타났다.

RSC prefetch 요청 수

진입 시점에 캡처한 요청에는 prefetch가 많이 포함되어 있었다.

start   wait   prefetch  segPrefetch          path
    0    151   -         -                    /workspace/<id>/board/list
  309     21   1         /_tree               /workspace?_rsc=...
  334     13   1         /workspace/__PAGE__  /workspace?_rsc=...
  334     16   1         /_index              /workspace?_rsc=...
  334     29   1         /_head               /workspace?_rsc=...
  386     44   1         /_tree               /workspace/<id>/board/archive?...
  466     43   1         /_tree               /workspace/<id>/board/stages?_rsc=...

정적 에셋을 제외한 요청 16건 중 15건이 prefetch였다. 전체 진입 기준으로는 오리진 Function까지 가는 왕복이 24건까지 잡혔다. Next 16 업그레이드 문서도 App Router prefetch의 방향을 설명한다. 공유 layout은 중복 다운로드하지 않고, 캐시에 없는 부분만 incremental prefetch로 가져온다. 대신 개별 prefetch 요청 수는 늘어날 수 있다. 전송량 관점에서는 이득일 수 있다. 하지만 각 요청이 Edge cache에서 끝나지 않고 오리진 Function까지 가는 배포에서는 요청 수가 왕복 비용으로 바뀐다.

Region 지연이 화면 지연으로 보이는 경로
  1. 요청 1건185ms

    Seoul Edge와 Washington Function 왕복 차이

  2. 워크스페이스 진입24건

    RSC 요청과 segment prefetch가 함께 발생

  3. 화면 완성+956ms

    일부 요청이 화면 안정 시점을 뒤로 민다

Link와 button의 차이

탭 전환은 Region 변경 뒤에도 오리진 왕복이 반복됐다. 원인은 탭 구현이었다.

<button type="button">탭 A</button>
<button type="button">탭 B</button>

Next.js의 자동 prefetch는 <Link>를 기준으로 동작한다. 좌측 패널의 링크는 viewport에 들어올 때 미리 당겨졌지만, 상단 탭은 <button> 기반이라 prefetch 대상에서 빠졌다.

프리페치 대상 차이
  • 좌측 패널<Link>
  • 상단 탭<button>

Region 변경은 요청 1건의 비용을 줄인다. 탭을 <Link> 기반으로 바꾸거나 필요한 시점에 router.prefetch()를 호출하면 왕복 횟수 자체를 줄일 수 있다.

측정 기준

화면 완성 시점은 오리진 요청의 in-flight 수가 0이 되고, DOM 변경이 700ms 동안 멈춘 시점으로 잡았다. 단순 FCP/LCP만 보면 초기 페인트는 보이지만, RSC와 prefetch가 끝나 실제 화면이 안정되는 시점은 놓칠 수 있다. 브라우저 측정값과 서버 로그를 함께 봐야 한다. 서버 로그는 Function 실행 시간과 Region 왕복을 보여주고, 브라우저 측정은 그 요청들이 화면 전환 안에서 몇 번 반복되는지 보여준다.

정리

Vercel Region 변경은 “서버 코드를 빠르게 만든 작업”이 아니다. 서울 사용자와 Washington Function 사이에 있던 오리진 왕복 비용을 제거한 작업이다. 이 프로젝트에서는 단일 요청 기준 185ms가 줄었고, RSC prefetch가 많은 워크스페이스 진입에서는 화면 완성 기준 약 956ms 차이로 나타났다. 성능 개선 지점은 두 층으로 나뉜다.

레버 줄이는 것
Function Region 오리진 왕복 1건의 비용
Link/prefetch/cache 구조 오리진 왕복 횟수

사용자가 특정 지역에 몰려 있고, 데이터 소스도 그 지역에 있으며, 오리진 Function까지 가는 요청이 많다면 Function Region은 화면 체감 속도에 직접 영향을 준다. 그 다음으로 볼 것은 Region이 아니라 요청이 몇 번 왕복하는지다.

관련 글