RSS듀오랩스
웹 인프라

TTFB: 속도가 아니라 지연 시간을 재는 지표

작성자
듀오랩스 대표·7분 읽기

회선을 두 배로 넓혀도 TTFB 는 거의 그대로입니다. 서버를 한 대 더 붙여도 마찬가지입니다. 그런데 같은 서버를 사용자와 가까운 리전으로 옮기면 절반으로 떨어집니다.

세 조치 모두 "성능 개선"이라는 한 단어로 묶이는데 결과가 전혀 다릅니다. 앞의 둘은 처리량을 건드렸고, TTFB 는 처리량을 재는 숫자가 아니기 때문입니다.

회선을 키워도 TTFB 가 안 움직이는 이유

처리량은 단위 시간에 몇 건을 끝내느냐이고, 지연 시간은 한 건이 끝나는 데 얼마가 걸리느냐입니다. 대역폭을 늘리는 것은 관을 굵게 만드는 일이지 짧게 만드는 일이 아닙니다.

거리는 요금제로 줄지 않습니다. 서울과 로스앤젤레스는 대권거리로 약 9,600km 이고, 광섬유 안에서 신호는 진공 광속의 3분의 2 남짓으로 갑니다. 왕복 한 번에 물리적으로 약 96ms 가 필요하다는 뜻입니다. 이 값은 대역폭을 얼마를 사든 바뀌지 않습니다.

반대 방향도 있습니다. 처리량을 올리려고 요청을 모아 배치로 돌리거나 큐에 쌓아 병렬로 소화하면, 전체 처리량은 올라가면서 개별 요청이 기다리는 시간은 길어집니다. 두 축은 같이 움직이지 않고 자주 서로를 밀어냅니다.

그래서 "느리다"는 증상이지 진단이 아닙니다. 어느 축이 문제인지 먼저 정해야 다음 조치가 정해집니다.

TTFB 가 재는 구간, startTime 에서 responseStart 까지

web.dev 의 정의는 이렇습니다.

TTFB is a metric that measures the time between starting navigating to a page and when the first byte of a response begins to arrive.

Navigation Timing 으로 말하면 startTime 에서 responseStart 까지입니다. 응답 본문이 다 오는 시간은 포함하지 않습니다. 첫 바이트가 도착하는 순간 측정이 끝납니다.

그 구간 안에 들어 있는 것이 생각보다 많습니다.

구간 성격
리다이렉트 왕복이 통째로 한 번 더
서비스 워커 기동 클라이언트
DNS 조회 왕복
연결과 TLS 협상 왕복
요청 전송부터 첫 바이트 도착까지 왕복 + 서버 처리

권장값은 0.8초 이하가 양호, 1.8초 초과가 나쁨입니다. 리다이렉트가 이 안에 들어 있다는 점은 따로 짚어 둘 만합니다. http 에서 https 로 한 번 튀는 것만으로 왕복 한 세트가 TTFB 에 그대로 얹힙니다.

한 숫자에 섞여 있는 네트워크 왕복과 서버 처리

TTFB 는 두 덩어리의 합입니다. 왕복 횟수 곱하기 왕복 시간, 그리고 서버가 응답을 만드는 시간.

문제는 이 둘의 대응이 정반대라는 점입니다. 왕복이 범인이면 엣지를 앞에 두거나 리전을 옮기거나 연결을 재사용해야 합니다. 서버 처리가 범인이면 쿼리를 고치거나 결과를 캐시하거나 콜드 스타트를 없애야 합니다. 한쪽 처방을 다른 쪽에 쓰면 아무 일도 일어나지 않습니다.

TTFB 숫자 하나로는 그 둘이 갈리지 않습니다. 저는 이것이 이 지표의 가장 큰 약점이라고 봅니다. 값이 나쁘다는 사실은 알려 주는데 무엇을 고쳐야 하는지는 말해 주지 않습니다.

왕복 쪽만 해도 편차가 큽니다. 연결을 새로 맺으면 DNS 한 번, TCP 한 번, TLS 협상이 또 한 번 필요하고, TLS 1.3 은 1-RTT 로 끝나지만 그 이전 버전은 2-RTT 를 씁니다. 이미 열린 연결을 재사용하면 이 왕복이 전부 0 이 됩니다. 같은 서버, 같은 페이지인데 첫 방문과 두 번째 방문의 TTFB 가 크게 벌어지는 이유가 여기 있습니다.

TTFB 를 좋게 만들면서 페이지는 더 느려지는 경우

이 지표를 목표로 잡으면 지표만 좋아지는 최적화를 부르기 쉽습니다. web.dev 도 응답을 미리 흘려보내는 방식을 언급합니다.

Some servers allow early flushing of the document response to happen before the main body is available, either with just the HTTP headers, or with the <head> element.

헤더나 <head> 만 먼저 내보내면 첫 바이트는 즉시 나갑니다. TTFB 는 확실히 좋아집니다. 본문이 준비되는 시점은 하나도 안 바뀌었는데 말이죠. 정의를 완벽히 만족시키면서 사용자가 기다리는 시간은 그대로인 상태입니다.

반대 방향의 함정도 같이 있습니다. 서버에서 HTML 을 만들어 보내면 TTFB 는 올라가지만, 브라우저가 할 일이 줄어 LCP 는 오히려 빨라집니다. web.dev 는 서버 렌더링 사이트가 TTFB 는 높아도 FCP 와 LCP 는 나을 수 있다고 적어 두었고, TTFB 자체는 Core Web Vitals 가 아니라는 점도 명시합니다.

저는 TTFB 를 목표가 아니라 바닥값으로 봅니다. 사용자가 실제로 느끼는 것은 LCP 와 INP 쪽이고, TTFB 는 그 아래 깔린 하한선입니다. TTFB 가 200ms 면 LCP 는 어떤 수를 써도 200ms 밑으로 못 내려갑니다. 하한선은 낮춰 두되 순위표로 쓰지는 않는 편이 맞습니다.

Server-Timing 으로 서버 몫만 떼어 내기

앞 절의 "안 갈린다"에 대한 답은 서버가 자기 몫을 직접 신고하게 만드는 것입니다. Server-Timing 헤더가 그 일을 합니다.

Server-Timing: db;dur=53, app;dur=47.2

이름과 dur(밀리초), 필요하면 desc 로 설명을 붙입니다. 브라우저는 이 값을 PerformanceServerTiming 으로 노출하고, 각각 name, duration, description 에 대응시킵니다. TTFB 에서 신고된 서버 시간을 빼면 남는 것이 대략 네트워크 몫입니다.

한 가지 걸리는 지점이 있습니다. 이 인터페이스는 기본적으로 동일 출처로 제한되어서, 다른 오리진의 응답을 JavaScript 로 읽으려면 서버가 Timing-Allow-Origin 을 내줘야 합니다. 자사 도메인이면 문제가 없지만 서드파티 자원의 서버 시간을 보려면 상대가 협조해야 합니다.

저라면 TTFB 를 대시보드에 단독으로 올리지 않고 항상 이 헤더와 짝지어 둡니다. 숫자 하나가 두 가지 원인을 가리키는 상태를 오래 두면, 급할 때 반드시 엉뚱한 쪽을 고치게 됩니다.

여기서부터는 구현마다 다릅니다

프록시나 CDN 이 앞에 있으면 신고되는 서버 시간이 오리진의 것인지 엣지의 것인지가 구성에 따라 갈립니다. 서버리스 환경의 콜드 스타트가 어느 구간에 잡히는지도 플랫폼마다 다릅니다. 이건 넘겨짚으면 안 되는 자리라고 봅니다.

측정 위치도 같은 문제를 안고 있습니다. 실험실에서 한 번 재는 값과 실제 사용자들의 분포는 다른 이야기입니다. p50 이 양호해도 p99 는 새 연결을 맺는 방문자 비율에 지배당하고, 그 비율은 캐시 정책과 유입 경로가 정합니다. 대표값 하나만 보고 있으면 이 차이가 보이지 않습니다.

이 개념이 어디쯤 놓이는지는 컴퓨터 공학 핵심 개념 지도에 정리해 두었습니다.

이 게시글 공유하기

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.