performance.now()로 코드 실행 시간 측정
함수 하나가 얼마나 걸리는지 재보고 싶어서 시작은 단순했다. 콘솔에 시간만 찍어보면 된다고 생각했는데, Date.now()와 performance.now() 사이에서 한 번 멈춰 섰다. 둘 다 시간을 돌려주는데, 코드 실행 시간 측정에서는 왜 굳이 performance.now()를 쓰는지 짚어두고 싶었다.
Date.now()가 아니라 performance.now()여야 했던 이유
Date.now()는 1970-01-01 UTC 기준 epoch 시각을 돌려주는 함수다. 두 시점을 빼면 경과 시간이 나오긴 한다. 다만 이 시계는 시스템 시계에 묶여 있다. NTP가 동기화로 시간을 뒤로 당기거나, 사용자가 수동으로 시간을 바꾸면 측정 도중에도 값이 흔들린다. 코드 실행 도중 시계가 뒤로 가면 경과 시간이 음수가 나오는 일이 생긴다.
performance.now()는 단조 증가(monotonic)한다. 시스템 시계가 어떻게 바뀌든 영향받지 않고, 호출할 때마다 이전보다 큰 값을 돌려준다. 코드 블록 두 지점의 차이로 실행 시간을 잴 때 안전한 쪽이다.
반환값이 뭔지부터
performance.now()가 돌려주는 값은 DOMHighResTimeStamp 타입이다. double precision 부동소수이고 단위는 밀리초다. 정수가 아니라 소수점이 붙은 ms 값이 나온다.
기준점은 performance.timeOrigin이다. Window 컨텍스트에서는 네비게이션이 시작된 시점, Worker 컨텍스트에서는 워커가 생성된 시점이 0이다. 그러니까 절대 시각이 아니라 "이 문서/워커가 시작된 뒤로 얼마나 지났는지"를 나타내는 값이다.
실제로 쓴 코드
측정하고 싶은 코드 블록을 두 호출 사이에 끼우는 방식이다.
const startTime = performance.now();
// 측정 대상 코드
const endTime = performance.now();
console.log(endTime - startTime);endTime - startTime이 그 블록의 실행 시간(ms)이다. 단조 증가가 보장되니까 이 차이는 항상 0 이상의 값으로 떨어진다.
정밀도는 브라우저마다 다르게 깎여 있다
여기서 한 가지 알아둘 게 있었다. performance.now()의 정밀도는 사양상 깎여 있다. Spectre 같은 사이드 채널 타이밍 공격을 막기 위해 브라우저가 의도적으로 라운딩을 건다.
표준 권고는 cross-origin isolated 환경에서 5μs, 그렇지 않은 환경에서 100μs 단위 라운딩이다. 환경마다 실제 적용 값은 또 다르다.
| 브라우저 | 라운딩 동작 |
|---|---|
| Chrome | v91부터 표준 권고 값(5μs / 100μs)에 정렬 |
| Safari (WebKit) | 1ms 단위로 더 거칠게 라운딩 |
| Firefox | privacy.reduceTimerPrecision 설정으로 단위 조정 가능 |
수 마이크로초짜리 차이를 재고 싶다면 이 한계를 알고 있어야 한다. 특히 Safari에서는 1ms 미만의 측정값은 의미가 거의 없다. 1ms 이상의 블록이라면 일반적인 코드 실행 시간 측정에는 충분하다.
더 구조화된 측정이 필요해진다면
단일 함수 한두 번이면 performance.now() 차이 계산으로 충분하다. 다만 여러 구간을 동시에 재고 싶거나, 측정 결과를 DevTools에서 같이 보고 싶을 때는 User Timing API가 더 맞는다. performance.mark()로 시점에 이름을 붙이고, performance.measure()로 두 마크 사이의 간격을 측정하는 방식이다.
performance.mark("loadStart");
// 측정 대상 코드
performance.mark("loadEnd");
performance.measure("load", "loadStart", "loadEnd");이렇게 측정한 결과는 PerformanceEntry 객체로 보존된다. performance.getEntriesByType("measure")로 조회할 수 있고, Chrome DevTools Performance 탭에도 같은 마크가 함께 잡혀서 시각적으로 어디서 시간이 빠지는지 확인할 수 있다.
Node.js에서도 똑같이 쓸 수 있다
서버 코드의 실행 시간을 잴 때도 같은 API를 그대로 쓸 수 있다. Node.js는 node:perf_hooks 모듈에서 performance.now()를 제공한다.
const { performance } = require("node:perf_hooks");
const startTime = performance.now();
// ...
const endTime = performance.now();
console.log(endTime - startTime);Node.js v16부터는 글로벌 performance 객체로도 접근할 수 있어서 모듈 임포트 없이 바로 호출할 수 있다.
console.time()이라는 더 가벼운 길
브라우저 콘솔에서 빠르게 확인만 하면 되는 상황이면 console.time() / console.timeEnd()가 더 간단하다.
console.time("load");
// 측정 대상 코드
console.timeEnd("load");console.timeEnd()가 호출되면 콘솔에 경과 시간이 그대로 찍힌다. 다만 반환값이 없고 콘솔 출력 외에 다른 곳에서 값을 받아 가공할 수가 없다. 측정값을 변수에 담아 서버로 보내거나, 평균을 계산하거나, 조건문에 쓰고 싶다면 결국 performance.now() 쪽으로 가게 된다.
코드 한 블록의 실행 시간만 빠르게 잴 거면 performance.now() 두 번 호출과 뺄셈 한 줄이 가장 가볍다. 단조 증가하고 시스템 시계에 흔들리지 않는다는 것만 알아둬도 Date.now()와의 갈림길에서 헷갈리지 않는다.