이름 맹글링으로 VS Code 축소
2023년 7월 20일 Matt Bierner, @mattbierner
최근 Visual Studio Code의 배포 JavaScript 크기를 20% 줄였습니다. 이는 3.9MB 이상 절감된 것입니다. 물론 릴리스 노트에 있는 개별 GIF 파일 몇 개보다는 적지만, 무시할 만한 수준은 아닙니다! 이 감소로 인해 다운로드하고 디스크에 저장해야 하는 코드의 양이 줄어드는 것뿐만 아니라, JavaScript가 실행되기 전에 스캔해야 하는 소스 코드의 양이 줄어들어 시작 시간도 개선됩니다. 코드를 전혀 삭제하지 않고 코드베이스에서 주요 리팩토링도 없이 이만큼의 감소를 달성했다는 점을 고려하면 꽤 괜찮습니다! 대신에 필요한 것은 새로운 빌드 단계였습니다. 바로 이름 축약(name mangling)입니다.
이 게시물에서는 어떻게 이 최적화 기회를 식별하고, 문제에 대한 접근 방식을 탐색했으며, 결국 이 20% 크기 감소를 구현했는지 공유하고 싶습니다. 축약의 구체적인 내용에 집중하기보다는 VS Code 팀에서 엔지니어링 문제를 접근하는 방식에 대한 사례 연구로 다루고 싶습니다. 이름 축약은 멋진 기술이지만 많은 코드베이스에서는 그만한 가치가 없을 수 있으며, 축약에 대한 저희의 구체적인 접근 방식은 개선될 가능성이 있습니다 (또는 프로젝트 구축 방식에 따라 전혀 필요하지 않을 수도 있습니다).
문제 식별
VS Code 팀은 성능에 열정을 쏟고 있으며, 이는 핫 코드 경로 최적화, UI 재배치 감소, 시작 시간 단축 등 모든 면에서 나타납니다. 이러한 열정에는 VS Code의 JavaScript 크기를 작게 유지하는 것도 포함됩니다. 데스크톱 애플리케이션 외에도 웹(https://vscode.dev)에서 VS Code가 배포되면서 코드 크기는 더욱 중요한 요소가 되었습니다. 코드 크기를 적극적으로 모니터링하면 VS Code 팀 구성원들이 변경 사항을 인지할 수 있습니다.
안타깝게도 이러한 변경 사항은 거의 항상 증가였습니다. VS Code에 어떤 기능을 구축할지에 대해 많은 고민을 하고 있지만, 수년에 걸쳐 새로운 기능을 추가하면서 필연적으로 배포하는 코드의 양이 늘어났습니다. 예를 들어, VS Code의 핵심 JavaScript 파일 중 하나(workbench.js)는 8년 전에 비해 현재 약 4배 커졌습니다. 8년 전 VS Code가 현재 많은 사람들이 필수적이라고 생각하는 — 편집기 탭이나 내장 터미널과 같은 — 기능이 없었다는 점을 고려하면, 이러한 증가는 생각만큼 끔찍하지 않을 수 있지만 그렇다고 아무것도 아닌 것도 아닙니다.

그 4배의 크기 증가는 이미 많은 지속적인 성능 엔지니어링 작업 이후의 결과입니다. 다시 말하지만, 이러한 작업은 주로 코드 크기를 추적하고 증가하는 것을 싫어하기 때문에 이루어집니다. 이미 코드를 esbuild를 통해 실행하여 압축하는 것을 포함하여 많은 쉬운 코드 크기 최적화를 수행했습니다. 몇 년 동안 추가적인 절감을 찾는 것은 점점 더 어려워졌습니다. 많은 잠재적인 절감 효과는 위험을 초래하거나 구현 및 유지 관리에 필요한 추가 엔지니어링 노력을 기울일 만큼 가치가 없습니다. 이는 JavaScript 크기가 서서히 증가하는 것을 지켜봐야 함을 의미합니다.
작년에 vscode.dev에서 압축된 소스 코드를 디버깅하던 중 놀라운 사실을 발견했습니다. 압축된 JavaScript에도 여전히 extensionIgnoredRecommendationsService와 같이 수많은 긴 식별자 이름이 포함되어 있었습니다. 이는 놀라웠습니다. esbuild가 이미 이러한 식별자를 단축했을 것이라고 가정했습니다. 그리고 esbuild는 실제로 "축약(mangling)"이라는 프로세스를 통해 일부 경우 식별자를 단축한다는 것을 알게 되었습니다 (JavaScript 도구가 컴파일 언어의 거의 유사한 프로세스에서 차용한 용어입니다).
압축 중 축약은 긴 식별자 이름을 단축하여 다음과 같은 코드를 변환합니다.
const someLongVariableName = 123;
console.log(someLongVariableName);
훨씬 더 짧은
const x = 123;
console.log(x);
JavaScript는 소스 텍스트로 배포되기 때문에 식별자 이름의 길이를 줄이는 것은 실제로 프로그램의 크기를 줄입니다. 컴파일 언어에서 오신 분이라면 이 최적화가 조금 바보 같아 보일 수 있지만, 여기 JavaScript의 멋진 세계에서는 어디에서든 찾을 수 있는 이러한 이점을 기꺼이 받아들입니다!
이제 모든 변수 이름을 한 글자로 바꾸러 달려가기 전에, 이러한 최적화는 신중하게 접근해야 함을 강조하고 싶습니다. 잠재적인 최적화가 소스 코드를 덜 읽기 쉽게 만들거나 유지 관리가 어렵게 만들거나 상당한 수동 작업을 요구한다면, 정말 엄청난 개선을 제공하지 않는 한 거의 가치가 없습니다. 여기저기서 약간의 바이트를 덜어내는 것은 좋지만, 이것이 엄청나다고 할 수는 없습니다.
이러한 최적화를 거의 무료로 얻을 수 있다면, 예를 들어 빌드 도구가 자동으로 수행하도록 한다면 계산이 달라집니다. 실제로 esbuild와 같은 스마트한 도구는 이미 식별자 축약을 구현하고 있습니다. 이는 우리가 veryLongAndDescriptiveNamesThatWouldMakeEvenObjectiveCProgrammersBlush와 같이 계속 작성하고 빌드 도구가 이를 자동으로 단축하도록 할 수 있다는 것을 의미합니다!
esbuild는 축약을 구현하지만, 기본적으로 축약이 코드 동작을 변경하지 않을 것이라고 확신하는 경우에만 축약합니다. 결국 번들러가 코드를 망가뜨리는 것은 정말 짜증 나는 일입니다. 실제로는 esbuild가 로컬 변수 이름과 인수 이름을 축약합니다. 이는 코드에서 정말 터무니없는 일을 하지 않는 한 안전합니다 (그런 경우, 코드 크기보다 훨씬 더 큰 문제를 가지고 있을 것입니다).
그러나 esbuild의 보수적인 접근 방식은 많은 이름을 안전하게 변경할 수 있다고 확신할 수 없기 때문에 건너뛴다는 것을 의미합니다. 간단한 예시로, 어떻게 잘못될 수 있는지 생각해 봅시다.
const obj = { longPropertyName: 123 };
function lookup(prop) {
return obj[prop];
}
console.log(lookup('longPropertyName'));
축약이 longPropertyName을 x로 변경하면 다음 줄의 동적 조회는 더 이상 작동하지 않습니다.
const obj = { x: 123 }; // Here `longPropertyName` gets rewritten to `x`
function lookup(prop) {
return obj[prop];
}
console.log(lookup('longPropertyName')); // But this reference doesn't and now the lookup is broken
위 코드에서 축약 중에 속성 자체가 변경되었음에도 불구하고 여전히 longPropertyName을 사용하여 속성에 접근하려고 하는 것을 알 수 있습니다.
이 예시는 인위적이지만, 실제 코드에서는 이러한 문제가 발생하는 여러 가지 방법이 있습니다.
- 동적 속성 접근.
- 객체 직렬화 또는 예상되는 객체 형태로 JSON 파싱.
- 노출하는 API (소비자는 새로운 축약된 이름을 알지 못합니다).
- 소비하는 API (DOM API 포함).
esbuild가 발견한 거의 모든 이름을 강제로 축약할 수는 있지만, 위의 이유로 인해 VS Code가 완전히 망가집니다.
그럼에도 불구하고 VS Code 코드베이스에서 더 나은 방법을 찾을 수 있다는 느낌을 떨칠 수 없었습니다. 모든 이름을 축약할 수는 없더라도, 안전하게 축약할 수 있는 일부 이름을 찾을 수는 있을 것이라고 생각했습니다.
비공개 속성에 대한 잘못된 시도
압축된 소스를 다시 살펴보니, _로 시작하는 긴 이름이 얼마나 많은지 눈에 띄었습니다. 관례적으로 이는 비공개 속성을 나타냅니다. 비공개 속성은 안전하게 축약할 수 있고 클래스 외부에서는 알 수 없을 것입니다, 맞죠? 그리고 esbuild가 이미 우리를 위해 이것을 하고 있어야 하지 않나요? 그러나 esbuild를 작성한 사람들은 게으르지 않다는 것을 알고 있었습니다. esbuild가 비공개 속성을 축약하지 않았다면, 거의 확실히 좋은 이유가 있었을 것입니다.
문제에 대해 더 생각해보니, 비공개 속성도 위 longPropertyName 예제에서 보여준 동일한 동적 속성 조회 문제에 영향을 받는다는 것을 깨달았습니다. 당신과 같은 똑똑한 TypeScript 프로그래머는 그런 코드를 절대 작성하지 않겠지만, 동적 패턴은 실제 코드베이스에서 충분히 일반적이어서 esbuild는 안전하게 플레이하기로 선택합니다.
TypeScript의 private 키워드는 실제로 예의 바른 제안에 불과하다는 점도 명심하십시오. TypeScript 코드가 JavaScript로 컴파일될 때 private 키워드는 기본적으로 제거됩니다. 이는 클래스 외부의 무례한 코드가 내부로 침입하여 비공개 속성에 임의로 접근하는 것을 막지 못한다는 것을 의미합니다.
class Foo {
private bar = 123;
}
const foo: any = new Foo();
console.log(foo.bar);
이러한 의심스러운 일을 직접적으로 하고 있지 않기를 바라지만, 속성 이름을 부주의하게 변경하면 객체 전개, 직렬화, 그리고 서로 다른 클래스가 공통 속성 이름을 공유하는 경우와 같은 다양한 예상치 못한 방식으로 문제가 발생할 수 있습니다.
다행히 VS Code를 작업하면서 한 가지 큰 이점을 발견했습니다. 저는 (대부분) 합리적인 코드베이스에서 작업하고 있었습니다. esbuild가 할 수 없는 많은 가정을 할 수 있었습니다. 예를 들어 동적 비공개 속성 접근이나 잘못된 any 접근이 없다는 것입니다. 이것은 제가 직면한 문제를 더욱 단순화했습니다.
그래서 Johannes Rieken(@johannesrieken)과 함께 비공개 속성 축약을 탐색하기 시작했습니다. 우리의 첫 번째 아이디어는 코드베이스 전체에서 JavaScript의 네이티브 #private 필드를 사용하는 것이었습니다. 비공개 필드는 위에 설명된 모든 문제에 면역일 뿐만 아니라 esbuild에 의해 자동으로 축약됩니다. 일반 JavaScript에 더 가까워지는 것도 매력적이었습니다.
하지만 이 접근 방식은 매개변수 속성 사용을 제거하는 것을 포함하여 대규모 (위험한) 코드 변경을 요구했기 때문에 신속하게 기각했습니다. 비교적 새로운 기능인 비공개 필드는 아직 모든 런타임에서 최적화되지 않았습니다. 이를 사용하면 무시할 수 있는 수준부터 약 95%까지 성능 저하를 유발할 수 있습니다! 장기적으로는 올바른 변경이 될 수 있지만, 지금 당장 필요한 것은 아니었습니다.
다음으로 esbuild가 주어진 정규 표현식과 일치하는 속성을 선택적으로 축약할 수 있다는 것을 발견했습니다. 하지만 이 정규 표현식은 식별자 이름과만 일치합니다. 따라서 속성이 TypeScript에서 private으로 선언되었는지 알 수는 없었지만, _로 시작하는 모든 속성을 축약하려고 시도할 수 있었고, 이는 비공개 및 보호된 속성만 포함할 것이라고 예상했습니다.
곧 _ 속성이 모두 축약된 작동하는 빌드를 얻었습니다. 좋습니다! 이는 비공개 속성 축약이 가능하며 상당한 절감을 가져왔지만, 예상했던 것보다는 적었습니다.
안타깝게도 이름만 기반으로 한 축약에는 심각한 단점이 있습니다. 여기에는 코드베이스의 모든 비공개 속성이 _로 시작해야 한다는 요구 사항이 포함됩니다. VS Code 코드베이스는 이 명명 규칙을 일관되게 따르지 않으며, _로 시작하는 공개 속성이 몇 군데 있습니다 (일반적으로 속성이 외부에서 접근 가능해야 하지만 API로 취급되어서는 안 되는 경우, 예를 들어 테스트에서 사용됩니다).
또한 축약된 코드가 실제로 올바르다고 완전히 확신하지 못했습니다. 물론 테스트를 실행하거나 VS Code를 시작해 볼 수 있었지만, 이는 시간이 많이 걸렸고 덜 일반적인 코드 경로를 놓쳤을 가능성은 없을까요? 다른 코드를 건드리지 않고 비공개 속성만 축약하고 있다는 것을 100% 확신할 수 없었습니다. 이 접근 방식은 너무 위험하고 채택하기에 너무 부담스러워 보였습니다.
TypeScript를 통한 자신 있는 축약
축약 빌드 단계에 더 자신감을 가질 수 있는 방법을 고민하던 중 새로운 아이디어를 떠올렸습니다. TypeScript가 축약된 코드를 검증해 줄 수는 없을까요? TypeScript가 일반 코드에서 알 수 없는 속성 접근을 감지할 수 있는 것처럼, TypeScript 컴파일러도 속성이 축약되었지만 참조가 올바르게 업데이트되지 않은 경우를 감지할 수 있어야 합니다. 컴파일된 JavaScript를 축약하는 대신, TypeScript 소스 코드를 축약하고 새로운 TypeScript를 축약된 식별자 이름으로 컴파일할 수 있습니다. 축약된 소스 코드에 대한 컴파일 단계는 코드를 실수로 망가뜨리지 않았다는 더 큰 확신을 줄 것입니다.
뿐만 아니라 TypeScript를 사용하면 private 속성을 진정으로 찾을 수 있습니다 (_로 시작하는 속성 대신). TypeScript의 기존 rename 기능을 사용하여 객체 모양을 예상치 못한 방식으로 변경하지 않고 기호를 스마트하게 이름을 바꿀 수도 있습니다.
이 새로운 접근 방식을 시도하고 싶어, 곧 다음과 같이 대략 작동하는 새로운 축약 빌드 단계를 고안했습니다.
for each private or protected property in codebase (found using TypeScript's AST):
if the property should be mangled:
Compute a new name by looking for an unused symbol name
Use TypeScript to generate a rename edit for all references to the property
Apply all rename edits to our typescript source
Compile the new edited TypeScript sources with the mangled names
그리고 다소 놀랍게도, 이런 단순해 보이는 접근 방식이 효과가 있었습니다! 물론 대부분은요.
TypeScript가 전체 코드베이스에 걸쳐 수천, 수만 건의 올바른 편집을 생성하는 능력에 매우 감탄했지만, 몇 가지 엣지 케이스를 처리하기 위한 로직도 추가해야 했습니다.
-
새로운 비공개 속성 이름이 현재 클래스에서 고유한 것만으로는 충분하지 않으며, 현재 클래스의 모든 상위 클래스 및 하위 클래스에서도 고유해야 합니다. 다시 한번 근본적인 원인은 TypeScript의
private키워드가 실제로 상위 클래스와 하위 클래스가 비공개 속성에 접근할 수 없도록 강제하지 않는 컴파일 타임 장식이라는 것입니다. 주의하지 않으면 이름 바꾸기가 이름 충돌을 일으킬 수 있습니다 (다행히 TypeScript는 이를 오류로 보고합니다). -
우리 코드의 일부에서는 하위 클래스가 상속된 보호된 속성을 공개했습니다. 이러한 대부분은 실수였지만, 이러한 경우 축약을 비활성화하는 코드를 추가했습니다.
이러한 경우에 대한 코드를 추가한 후, 곧 작동하는 빌드를 확보했습니다. 비공개 속성을 축약함으로써 VS Code의 메인 workbench.js 스크립트 크기가 12.3MB에서 10.6MB로 약 14% 감소했습니다. 또한 소스 텍스트를 스캔할 양이 줄어들어 코드 로딩 속도가 5% 향상되었습니다. 소스 코드의 몇 가지 매우 사소한 안전하지 않은 패턴 수정 외에, 이러한 절감이 거의 무료였다는 점을 고려하면 전혀 나쁘지 않습니다.
배운 점 및 추가 작업
비공개 속성 축약은 대규모 코드 변경이나 비용이 많이 드는 재작성에 의존하지 않고도 VS Code에서 상당한 개선을 여전히 찾을 수 있음을 보여줍니다. 이 경우, 다른 사람들이 수년에 걸쳐 VS Code의 압축된 소스를 살펴보고 긴 이름에 대해 궁금해했을 것이라고 생각합니다. 하지만 이를 안전하게 처리하는 것은 불가능해 보였거나, 혹은 엄청난 엔지니어링 투자가 필요해 보였을 수 있습니다.
이번 성공의 핵심은 이름 축약이 안전할 가능성이 높고 최적화가 여전히 상당한 개선을 가져올 경우 (비공개 속성)를 식별하는 것이었습니다. 그런 다음 이 변경을 최대한 안전하게 수행할 수 있는 방법을 생각했습니다. 이는 먼저 TypeScript 도구를 사용하여 자신 있게 식별자 이름을 바꾸고, 그런 다음 다시 TypeScript를 사용하여 새로 축약된 소스 코드가 올바르게 컴파일되는지 확인하는 것을 의미했습니다. 그 과정에서 우리 코드가 이미 대부분의 TypeScript 모범 사례를 따르고 있고 일반적인 VS Code 코드 경로를 다수 커버하는 테스트가 있었다는 사실이 큰 도움이 되었습니다. 이 모든 것이 합쳐져서 Joh와 저는 다른 VS Code 개발자들에게 거의 영향을 미치지 않는 상당히 급진적인 변경을 잉여 시간에 구현할 수 있었습니다.
하지만 축약 이야기는 여기서 끝나지 않습니다. 새로 축약되고 압축된 소스를 살펴보면서 provideWorkspaceTrustExtensionProposals와 같은 많은 다른 긴 이름을 보고 실망했습니다. 가장 주목할 만한 것은 UI에 표시되는 문자열에 사용하는 함수인 localize의 거의 5000번 사용이었습니다. 분명 개선의 여지가 있었습니다.
비공개 속성 축약과 동일한 접근 방식과 기술을 사용하여, 곧 또 다른 일반적인 코드 패턴을 식별했는데, 이는 높은 투자 수익으로 안전하게 축약할 수 있었습니다. 바로 내보낸 심볼 이름입니다. 내보내기가 내부적으로만 사용되는 한, 코드 동작을 변경하지 않고도 이를 단축할 수 있다고 확신했습니다.
이는 대체로 올바랐지만, 몇 가지 복잡한 문제가 다시 발생했습니다. 예를 들어, 확장 프로그램이 사용하는 API를 실수로 건드리지 않도록 해야 했고, TypeScript에서 내보냈지만 (일반적으로 워커 스레드 또는 프로세스의 진입점인) 형식화되지 않은 JavaScript에서 호출되는 일부 심볼도 제외해야 했습니다.
내보내기 축약 작업은 지난 반복에서 배포되었으며, workbench.js의 크기를 10.6MB에서 9.8MB로 더욱 줄였습니다. 모든 축약으로 인해 이 파일은 이제 축약이 없었을 때보다 20% 작아졌습니다. VS Code 전체에서 축약은 컴파일된 소스에서 3.9MB의 JavaScript 코드를 제거합니다. 이는 다운로드 크기와 설치 크기 모두에 대한 좋은 감소일 뿐만 아니라, VS Code를 시작할 때마다 스캔해야 하는 JavaScript의 양이 3.9MB 줄어든 것입니다.
이 차트는 시간이 지남에 따라 workbench.js의 크기를 보여줍니다. 오른쪽의 두 가지 감소를 주목하십시오. VS Code 1.74의 첫 번째 큰 감소는 비공개 속성 축약의 결과입니다. 1.80의 두 번째 작은 감소는 내보내기 축약에서 비롯되었습니다.


우리의 축약 구현은 압축된 소스에 여전히 많은 긴 이름이 포함되어 있으므로 의심할 여지 없이 개선될 수 있습니다. 만약 가치가 있고 안전한 접근 방식을 고안할 수 있다면 이를 더 조사할 수 있습니다. 이상적으로는 언젠가 이 작업의 많은 부분이 필요 없어질 것입니다. 네이티브 비공개 속성은 이미 자동으로 축약되며, 빌드 도구는 전체 코드베이스에 걸쳐 코드를 최적화하는 데 더 능숙해질 것입니다. 현재의 축약 구현을 검토할 수 있습니다.
우리는 항상 VS Code와 코드베이스를 더 좋게 만들기 위해 노력하고 있으며, 축약 작업은 우리가 이를 어떻게 접근하는지를 잘 보여주는 예라고 생각합니다. 최적화는 일회성 이벤트가 아니라 지속적인 과정입니다. 코드 크기를 지속적으로 모니터링함으로써 시간이 지남에 따라 어떻게 증가했는지 인지하고 있습니다. 이러한 인식은 의심할 여지 없이 코드 크기가 늘어나는 것을 더 많이 막는 데 도움이 되었고, 항상 개선점을 찾도록 격려합니다. 축약은 매력적으로 보였지만, 처음에는 너무 위험하여 심각하게 고려할 수 없었습니다. 위험을 줄이고, 적절한 안전망을 만들고, 축약을 채택하는 비용을 거의 제로로 만든 후에야 빌드에 적용할 자신감을 갖게 되었습니다. 최종 결과에 대해 정말 자랑스럽고, 그것을 달성한 방식에 대해서도 마찬가지입니다.
행복한 코딩 되세요,
Matt Bierner, VS Code 팀원 @mattbierner
축약 구현에 핵심적인 역할을 한 Johannes Rieken, 안전하게 축약을 구현할 수 있는 도구를 만들어준 TypeScript 팀, 매우 빠른 번들러인 esbuild, 그리고 이러한 최적화에 적합한 코드베이스를 구축한 전체 VS Code 팀에게 감사드립니다. 그리고 마지막으로, 우리가 그들에게 던지는 끔찍하게 축약된 JavaScript 더미에도 불구하고 항상 우리를 빠르게 보이게 해주는 V8 팀과 다른 모든 JS 엔진에 큰 감사를 표합니다.