노트

롱 태스크

Long Task

프론트엔드#performance#browser · 연결된 개념 8개

쉽게 말하면

롱 태스크는 계산대 직원 한 명이 손님 한 명 계산에 한참 붙잡혀 있는 상황이에요. 그동안 뒤에 선 손님은 아무리 급해도 기다려야 해서, 화면이 클릭에 반응하지 않는 것처럼 느껴져요.

비유의 한계 한 손님이 오래 걸리는 것만 문제는 아니에요. 50ms보다 짧은 태스크도 여러 개 이어지면 한 프레임을 막을 수 있어서, 그런 경우는 LoAF로 따로 봐요.

메인 스레드를 50ms 넘게 붙잡는 작업. W3C Long Tasks 명세는 이벤트 루프 태스크(뒤따르는 마이크로태스크 처리 포함), 렌더링 갱신 단계, 이벤트 루프 밖에서 브라우저가 UI 스레드로 하는 일 가운데 50ms를 넘는 것을 롱 태스크로 본다. 태스크는 도중에 끊기지 않으므로 그동안 클릭·키 입력 처리와 화면 갱신이 모두 밀린다(이벤트 루프).

왜 50ms인가

RAIL 모델(Response, Animation, Idle, Load)은 입력에 100ms 안에 반응하는 것을 목표로 둔다. 근거로는 지연 인식에 관한 UX 연구를 드는데, 링크한 NN/g 글에서 Jakob Nielsen은 0.1초를 시스템이 즉시 반응한다고 느끼는 한계로 본다.

목표가 100ms인데 예산이 50ms인 이유는 입력이 이미 실행 중인 작업 도중에 들어올 수 있어서다. 작업을 50ms 단위로 끊어 두면 입력은 최대 50ms 기다리고, 처리에 남은 50ms를 쓸 수 있다. 명세도 같은 논리로 롱 태스크가 없는 사이트는 100ms 목표를 지킬 수 있다고 설명한다. 다만 web.dev는 지금은 RAIL보다 Core Web Vitals를 성능 목표의 기준으로 권한다.

지표와의 관계

  • TBT(Total Blocking Time): FCP(First Contentful Paint) 뒤에 일어난 각 롱 태스크에서 50ms를 넘긴 부분(blocking time)만 더한다. Lighthouse는 기본으로 TTI(Time to Interactive)까지 잰다. 250ms·90ms·35ms 태스크라면 200 + 40 + 0 = 240ms다. 점수에서의 비중은 Lighthouse 성능 점수 읽기에 정리했다
  • INP(Interaction to Next Paint): 상호작용 지연은 입력 지연·처리 시간·표시 지연으로 나뉜다. 입력 직전에 시작된 롱 태스크는 이벤트 콜백이 시작되기까지의 입력 지연을 늘린다. 페이지 로드 중에는 큰 스크립트의 파싱·컴파일·실행이 롱 태스크가 되기 쉽다
  • TBT는 랩에서 INP 문제를 짐작하는 대리 지표다. 실제 사용자가 겪지 않는 문제를 잡거나, 상호작용이 일으키는 지연을 놓칠 수 있다

찾는 법

PerformanceObserver로 longtask 엔트리(PerformanceLongTaskTiming)를 받는다. buffered: true를 주면 관찰을 시작하기 전에 이미 일어난 롱 태스크도 받는다.

let blocking = 0
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    blocking += entry.duration - 50   // 50ms를 넘긴 부분
    console.log(entry.name, entry.duration, entry.attribution)
  }
}).observe({ type: 'longtask', buffered: true })

이 합은 TBT와 측정 구간이 달라(FCP~TTI로 자르지 않는다) 같은 값이 아니다. 엔트리의 attribution은 어느 컨테이너(최상위 문서, <iframe> 등)에서 일어났는지만 알려 주고, 어떤 스크립트·함수가 원인인지는 알려 주지 않는다.

Chrome DevTools Performance 패널에서는 롱 태스크에 빨간 삼각형이 붙고, 50ms를 넘긴 부분이 빨갛게 칠해진다.

Long Animation Frames(LoAF)

Long Tasks API의 한계는 셋이다. 원인 스크립트를 모른다. 렌더링이 별도 태스크로 돌아 그 렌더링을 일으킨 작업과 함께 세지 않는다. 50ms 아래 태스크 여러 개가 한 프레임을 막는 경우는 잡지 못한다.

LoAF(long-animation-frame 엔트리)는 태스크 대신 렌더링 갱신이 50ms 넘게 늦어진 프레임을 단위로 본다. 렌더링 시간이 포함되고, 5ms 넘게 실행된 스크립트마다 sourceURL·sourceFunctionName·invoker 같은 귀속 정보를 준다. blockingDuration은 프레임 안 롱 태스크에서 50ms를 넘긴 부분을 더한 값이다(가장 긴 태스크에는 렌더링 시간을 더해 계산한다).

지원(2026 기준): LoAF는 Chrome 123부터, Long Tasks API는 Chrome 58부터 쓸 수 있다. 둘 다 Chromium 계열에만 있고 Firefox·Safari는 지원하지 않는다. MDN은 둘 다 실험적 기능으로 표시한다. Chrome 팀은 LoAF가 더 나은 API라고 보면서도 Long Tasks API를 폐기할 계획은 없다고 밝혔다. 지원하지 않는 브라우저를 위해 PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')로 확인한 뒤 관찰한다.

고치는 법

  • 작업을 쪼개고 조각 사이에 태스크로 양보한다(scheduler.yield(), 없으면 setTimeout). 코드는 마이크로태스크의 "굶김을 피하려면" 절에 있다. web.dev는 isInputPending()은 더 이상 권하지 않고, 입력 대기 여부와 상관없이 양보하라고 한다
  • 무거운 계산은 메인 스레드 밖 Web Worker로 옮긴다
  • React 렌더가 길면 동시성 렌더링으로 렌더 작업을 잘게 나눈다

실행할 일을 줄이는 것 말고 언제 실행하느냐를 바꾸는 방법도 있다(개인적인 판단). 분석 스크립트처럼 급하지 않은 작업을 스텁 큐로 받아 두고 본체를 하이드레이션 뒤로 미루면, 총 실행 시간은 같아도 첫 화면·하이드레이션 구간의 롱 태스크와 메인 스레드 경합이 빠진다. 다만 미룬 작업도 롱 태스크라서 실행되는 순간 들어온 입력은 똑같이 기다린다. 미룬 시점이 FCP~TTI 안이면 TBT에도 그대로 잡힌다. 실행 시점이 바뀌어 TBT가 움직이는 다른 사례는 Lighthouse 성능 점수 읽기에 있다.

명세: W3C Long Tasks API — Terminology · Introduction

출처: web.dev — RAIL: 50 ms or 100 ms? · NN/g — Response Times: The 3 Important Limits · web.dev — Optimize long tasks · web.dev — Total Blocking Time · web.dev — Optimize INP: input delay · MDN — PerformanceLongTaskTiming · MDN — Long animation frame timing · Chrome — Long Animation Frames API · Status of the API · Will this replace the Long Tasks API? · Chrome DevTools — Performance features reference

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • localStorage와 sessionStorage

    브라우저에 문자열 키-값을 저장하는 Web Storage API의 두 가지. 쿠키와 달리 요청에 자동으로 실리지 않고, 출처 단위로 나뉘며(same-origin-policy), 보통 출처당 5MB 안팎을 쓸 수 있다. 동기 API라 큰 데이터를 자주 읽고 쓰면 메인 스레드를 막는다.

  • 동기·비동기와 블로킹·논블로킹

    동기·비동기는 결과를 언제 어떻게 받느냐, 블로킹·논블로킹은 기다리는 동안 호출한 스레드가 멈추느냐의 문제다.

  • loading 경계와 레이아웃 끌어올리기

    Next.js App Router에서 loading.tsx는 같은 세그먼트의 page와 그 아래(하위 layout 포함)를 suspense로 감싸지만, **같은 세그먼트의 layout은 감싸지 않는다**. 그래서 데이터를 기다릴 필요가 없는 공통 UI(탭바, 필터 헤더)를 page에서 layout으로 끌어올리면 스켈레톤에 덮이지 않고 처음부터 실제 모습으로 보인다.

  • requestAnimationFrame

    "다음 화면을 그리기 직전에 이 함수를 실행해 달라"고 브라우저에 예약하는 API. 콜백은 태스크 큐도 마이크로태스크 큐도 아닌 별도 목록에 들어가고, 이벤트 루프의 렌더링 단계에서 페인트 직전에 한꺼번에 실행된다.

  • 도허티 임계

    컴퓨터와 사용자가 서로를 기다리지 않는 속도, 대략 400ms 이내로 반응하면 생산성이 급격히 높아진다는 원칙. 1982년 IBM의 월터 도허티와 아르빈드 타다니의 논문에서 나왔다. 반응이 이 선 아래로 내려가면 사용자가 흐름을 잃지 않는다.

보기 옵션