[번역] 터보팩은 정말로 Vite 보다 10배는 더 빠를까? [Is Turbopack really 10x Faster than Vite?]

윤상준·2022년 12월 7일
1

아티클 번역

목록 보기
2/2
post-thumbnail

10월 1일 글.
저자 : yyx990803
원글 : Is Turbopack really 10x Faster than Vite? #8

들어가며

1주일 전에 Vercel은 터보팩을 공개했습니다. Rust로 구성되어있고 웹팩의 후계자로 말이죠.

발표의 헤드라인 중에서 터보팩이 "Vite 보다 10배 더 빠르다." 라는 내용이 있었습니다. Vercel은 홍보 과정에서 이 헤드라인 한 줄을 계속해서 반복 강조했습니다. 트위터, 블로그 포스트 그리고 Vercel 유저들에게 보내는 이메일에서도 말이죠. 터보팩 공식 문서에는 벤치마크 그래프도 나와있습니다. 이 그래프에는 터보팩을 장착한 Next 13은 React Hot-Module Replacement (HMR) 를 0.01초 만에 할 수 있다고 나와있습니다. 반면 Vite는 0.09초가 걸리죠. 또 이전에는 공식 문서에 Cold Start Performance 에 대한 벤치마크도 있었습니다만 10x 버전 이후부터는 없어졌습니다. 따라서 우리는 "10배 더 빠르다" 라는 주장은 HMR 성능에만 한정된 것이라고 추측할 수 밖에 없겠네요.

Vercel은 홍보 자료와 공식 문서에서 여러가지 수치들을 주장하고 있습니다만 정작 벤치마크 자체를 볼 수 있는 링크는 제공하지 않고 있습니다. 따라서 저는 이번에 릴리즈된 Next 13과 Vite 3.2를 갖고 직접 벤치마크를 해보기로 결정했습니다. 코드와 방법론은 이 곳에서 확인할 수 있습니다.

제 방법론의 요지는 다음과 같은 2개 타임스탬프 사이에서 델타를 측정하여 HMR 성능을 비교하는 것입니다.

  1. 소스 파일이 수정되는 시간. (파일 변경을 감시하는 별도의 Node.js 프로세스를 통해 기록됩니다.)
  2. 업데이트 된 React 컴포넌트가 리렌더링되는 시간. 컴포넌트의 렌더링 결과물에서 바로 Date.now()를 호출하여 측정합니다. 컴포넌트의 가상 DOM 렌더링 단계에서 호출하기 때문에 React의 재조정이나 실제 DOM 업데이트에는 영향을 받지 않습니다.

또한 다음과 같은 2개의 서로 다른 케이스에서 수치를 측정합니다.

  1. "루트 (root)" 케이스. 1000개의 서로 다른 자식 컴포넌트를 import 하고 같이 렌더링하는 역할의 컴포넌트.
  2. "리프 (leaf)" 케이스. 루트에서 import 되는 컴포넌트이면서 자식 컴포넌트 또는 import 문이 없는 컴포넌트.

뉘앙스 (Nuances)

본격적으로 수치를 측정하기 전에 몇 가지 추가적인 뉘앙스를 말씀드려야겠습니다.

  1. Next 가 React 서버 컴포넌트 (RSC)를 사용하는지 여부.
  2. Vite가 React transforms에 대해서 Babel 대신에 SWC를 사용하는지 여부.

React 서버 컴포넌트

Next 13은 중대한 구조적인 전환을 발표했습니다. 컴포넌트들은 기본적으로 서버 컴포넌트가 됐습니다. 사용자가 직접 'use client' 를 명시하여 클라이언트 모드를 선언하지 않는 이상 말이죠. 뿐만아니라, Next 공식 문서는 사용자 성능 향상을 위해 가능하면 서버 모드에 남아있을 것을 권장하고 있습니다.

제 첫 번째 벤치마크는 루트 케이스와 리프 케이스를 서버 모드에서 실행한 후, Next 13의 HMR 성능을 측정했습니다. 결과는 Next 13이 오히려 2개의 케이스에서 더 느리다고 나왔습니다. 그리고 리프 컴포넌트에서 그 차이점은 더 명확하게 나타납니다.

Round 1 스냅샷 (Next w/RSC, Vite w/Babel)

제가 이 수치를 트위터에 올리자마자 바로 지적받은 것은, 형평성을 위해서 RSC를 사용하지 않는 Next 컴포넌트도 측정해야한다는 점이었습니다. 따라서 저는 클라이언트 모드를 사용하기 위해 Next의 루트 컴포넌트에 use client를 추가했습니다. 확실히 클라이언트 모드에서는 Next의 HMR 성능이 더 향상되었고 Vite 보다 2배 정도 더 빨라졌습니다.

Round 2 스냅샷 (Next w/o RSC, Vite w/Babel)

SWC vs. Babel Transforms

우리의 목표는 HMR 성능 차이에만 집중하여 벤치마킹하는 것입니다. 비교 대상을 명확하게 하기 위해서, 우리는 "Vite의 기본 React 프리셋은 React HMR과 JSX를 변환하기 위해서 Babel을 사용한다" 라는 변수를 제거해야 합니다.

React HMR과 JSX 변환은 빌드 도구에 연결된 기능들이 아닙니다. Babel(자바스크립트 기반) 과 SWC(Rust 기반) 에 의해 이루어질 수 있죠. ESbuild도 JSX를 변환할 수 있지만 HMR을 지원하지 않습니다. SWC는 확실히 Babel보다 더 빠릅니다. (싱글 쓰레드에서는 20배, 멀티 코어에서는 70배 더 빠릅니다.) Vite가 현재 Babel로 기본 설정되어 있는 이유는 설치 용량과 실용성 사이의 균형 때문입니다. SWC의 설치 용량은 꽤 무겁습니다. (node_modules 에 58MB를 차지하죠. 반면 Vite 자체는 19MB밖에 차지하지 않습니다.) 또한 많은 사용자들이 여전히 다른 변환을 위해 Babel에 의존하고 있습니다. 따라서 Babel 패스는 그들에게 다소 불가피했을 것입니다. 하지만 미래에는 어떻게 바뀔 지 모르겠습니다.

더 중요한 것은, 같은 벤치마크에서 웹팩의 실행도 마찬가지로 SWC를 사용하고 있다는 점입니다.

Vite의 코어는 Babel에 의존하지 않습니다. Babel 대신에 SWC를 사용해서 React 변환을 관리하면 Vite 자체적으로는 아무런 변경 사항이 필요 없습니다. 그저 기본적인 React 플러그인을 vite-plugin-swc-react-refresh 로 교체하는 것 뿐입니다. 전환한 후 보면 루트 케이스에서 Vite의 성능이 크게 향상되어 Next를 따라잡은 것을 볼 수 있습니다.

흥미롭게도, 이 상승 곡선을 보면 터보팩을 장착한 Next가 루트 케이스에서 리프 케이스 대비 4배 더 느려졌다는 것을 알 수 있습니다. 이는 Vite의 HMR이 더 큰 규모의 컴포넌트에서도 더 잘 스케일링된다는 것을 의미합니다.

서로 다른 하드웨어에서의 성능

지금까지의 과정은 Node.js와 네이티브 Rust를 모두 포함하는 복합적인 벤치마크였기 때문에, 다른 하드웨어에서 큰 차이가 생길 수 있을 것입니다. 이 게시물에서의 수치들은 모두 제 M1 맥북 프로에서 나온 것들입니다. 다른 사용자들이 다른 하드웨어에서 똑같은 벤치마크를 돌렸을 때는 또 다른 수치가 나왔죠. 어떤 경우는 Vite가 루트 케이스에서 더 빨랐고, 또 어떤 경우에서는 Vite가 두 케이스에서 훨씬 더 빠르다는 결과가 나왔습니다.

Vercel의 해명

제 벤치마크가 포스팅된 이후, Vercel은 그들의 벤치마크 방법론이 담긴 블로그 게시물을 올렸고 Public에서도 벤치마크를 볼 수 있도록 해놓았습니다. 이건 사실 첫날에 해야 했던 일이었습니다만 그래도 확실히 옳은 방향으로 흘러가는 것 같습니다.

Vercel의 게시물과 벤치마크 코드를 읽어 보면 몇 가지 주목해야할 점이 있습니다.

  1. 벤치마크에서 Vite는 여전히 기본 세팅인 Babel 기반의 React 플러그인을 사용하여 실행됩니다. 반면 터보팩과 웹팩은 SWC를 사용하죠. 이 벤치마크는 기본적으로 불공평합니다.

  2. 1000개의 컴포넌트 케이스의 원래 수치가 반올림되는 이슈가 있었습니다. 터보백은 원래 15ms였는데 0.01s로 반올림되었고 Vite는 87ms였지만 0.09s로 반올림되었습니다. 또한 이는 약 6배 정도밖에 되지 않는 수치임에도 Vercel은 10배의 차이가 있다고 홍보했습니다.

  3. Vercel의 벤치마크는 업데이트된 모듈의 "브라우저 평가 시간 (browser eval time)" 을 타임스탬프의 끝으로 사용했습니다. React 컴포넌트의 리렌더링 시간이 아니라요. 전자는 이론적인 타임스탬프이지만, 후자는 사용자가 실제로 인식하는 엔드 투 엔드 HMR 업데이트 속도를 의미합니다. 저는 또한 Vite에 대한 평가와 업데이트 타임 스탬프를 수집하는 방식에서 결함을 발견했습니다. (마지막 업데이트 참조)

  4. Vercel의 게시물에는 전체 모듈 수가 30,000개를 초과할 경우 터보팩이 Vite보다 10배 더 빠르다는 그래프가 있습니다. 하지만, 두 케이스 모두 실제로는 10,000개의 모듈 까지만 측정하고 있었습니다. 그저 Vite의 HMR 곡선이 상승하기 시작할 때에 20,000개의 모듈을 초과했던 것이 전부였습니다. Vite 어플리케이션이 의존성을 사전-번들링 (Pre-Bundling)한다는 것을 고려하면, 20,000개 이상의 모듈을 사용하는 프로젝트는 매우 비현실적입니다. 10배 더 빠르다는 걸 증명하기 위해 30,000개를 사용하는건 본인들한테 유리한 것만 보여주는 것 같은 느낌이네요.

정리하자면, "Vite 보다 10배 더 빠르다"는 주장은 다음의 조건을 갖습니다.

  1. Vite가 똑같은 SWC 변환을 사용하지 않는 경우.
  2. 어플리케이션이 30,000개의 모듈을 포함하고 있을 경우.
  3. 벤치마크가 방금 업데이트된 모듈이 평가되는 시간만 측정하고, 그 변화가 실제로 적용될 때는 측정하지 않은 경우.

"공정한" 비교는 뭘까?

우리가 비교하려고 하는 것이 "별도의 설치나 구성이 필요 없이 바로 사용할 수 있는 기본값"이라면, 우리는 Next에서 활성화되어있는 React 서버 컴포넌트와 비교해야 합니다. 왜냐하면 그게 기본값이고 Next가 사용자들에게 사용을 권장하는 것이기 때문입니다. Vercel의 벤치마크가 React 서버 컴포넌트를 사용하고있지 않고, 또한 React의 HMR 런타임에서 발생하는 변수를 제외하기 위해 "모듈 평가 시간"을 측정하고 있기 때문에, 벤치마크의 목적은 Vite와 터보팩에 내재된 HMR 메커니즘을 정확하게 비교하는 것이라고 가정하는 것이 타당합니다.

이러한 전제에서는, 불행히도, 벤치마크에서 Vite가 여전히 Babel을 사용하고 있다는 점은 공평하지 않으며, 10배 더 빠르다는 주장은 여전히 무효합니다. Vite가 SWC 변환을 사용할 경우의 수치로 업데이트 되기 전까지 Vercel의 주장은 부정확한 것으로 간주해야 합니다.

덧붙여서, 아마 여러분 중 대부분은 다음의 주장에 동의할 것입니다.

  • 30,000개의 모듈은 대부분의 사용자들에게 매우 비현실적인 경우입니다. SWC를 사용하는 Vite라면 10배 더 빠르다는 주장에 필요한 모듈의 수는 훨씬 더 비현실적으로 증가할 것입니다. 이론적으로는 가능할지 몰라도, 이를 홍보 용도로 사용했던 Vercel의 모습은 진솔하지 못한 것처럼 보입니다.

  • 사용자들은 엔드 투 엔드 HMR 성능 즉, 저장하고 변경사항이 실제로 반영되는 시간에 더 신경을 많이 씁니다. "10배 더 빠르다"라는 주장을 보면, 일반적인 사용자는 후자 대신 전자의 관점에서 생각할 것입니다. Vercel은 홍보 과정에서 편리하게 이 주의사항을 생략했습니다. 실제로는, Next의 서버 컴포넌트에서의 엔드 투 엔드 HMR (기본값)은 Vite의 그것보다 더 느립니다.

Vite의 제작자로써, Vercel과 같이 자금이 많은 회사가 프론트엔드 도구를 개선하기 위해서 큰 투자를 하는 것은 매우 기쁩니다. 우리는 심지어 적용 가능한 부분이 있다면 터보팩을 Vite에서 활용할 수도 있습니다. 저는 OSS 분야에서의 건강한 경쟁은 결과적으로 모든 개발자들에게 이득이 될 것이라고 믿습니다.

하지만, OSS 경쟁은 공개된 커뮤니케이션과 공평한 비교, 그리고 상호간의 존중에 기반되어야 한다고도 믿습니다. 본인들에게 유리한 것들만 뽑아내서 홍보하고, 피어 리뷰도 없으며, 전형적인 상업적 경쟁에서만 찾아볼 수 있는 오해할만한 수치들에 대해서는 상당히 실망이고 또 우려가 됩니다. OSS에서 크게 성공한 회사로서, 저는 Vercel이 지금보다 더 잘할 수 있다고 믿습니다.

업데이트

Vercel의 벤치마크를 직접 클론하여 실행하고, Vite와 SWC를 사용해서 벤치마크를 업데이트 했습니다. 벤치마크는 또한 hmr_to_commit 메트릭스를 포함하고 있습니다만 벤치마크의 README에서만 언급됐군요. 벤치마크를 실행해보니, hmr_to_evalhmr_to_commit는 Vite와 정확하게 같은 수치 (~100ms)를 보여주는 반면, 터보팩에서는 15ms에서 54ms로 상승하는 것을 발견했습니다. 커밋 비용에 대한 평가가 있기 때문에 (Since the eval to commit cost is real), 유일하게 합리적인 설명은 벤치마크에 결함이 있다는 것 뿐입니다. hmr_to_eval은 Vite의 평가 시간을 정확하게 잡아내는데 실패했고 (아마 네이티브 ESM의 메카니즘 차이 때문인 것 같습니다.) 대신 hmr_to_commit 수치를 보고했습니다. 그렇지 않다면, 변경 사항을 커밋하는데 발생하는 오버헤드가 왜 Vite가 아닌 터보팩에만 적용되는지를 설명할 수 없습니다.

다음의 내용을 고려해주세요.

  • hmr_to_eval에는 결함이 있습니다.
  • hmr_to_commit이 사용자가 실제로 느끼는 엔드 투 엔드 성능을 더 잘 표현합니다.

hmr_to_commit을 측정 메트릭스로 사용하는게 더 맞는 것 같습니다. 이렇게하면 속도 이점이 2배 (~100ms vs. ~54ms) 미만으로 줄어들 것이며, 이는 지금 제가 벤치마크에서 보고 있는 수치와 일치합니다.

profile
하고싶은건 많은데 시간이 없다!

0개의 댓글