트리 쉐이킹 리액트 네이티브 앱

Nov 28 2022
웹 앱에 일반적인 최적화 기술을 React Native 앱에 적용하여 시작 시간을 20% 단축한 방법. 나무 무엇? 틀림없이 Tree Shaking은 혼란스러운 용어일 수 있습니다.

웹 앱에 일반적인 최적화 기술을 React Native 앱에 적용하여 시작 시간을 20% 단축한 방법.

나무 무엇?

틀림없이 Tree Shaking은 혼란스러운 용어일 수 있습니다. TypeScript에서 "import elision"이라고 이미 들어보셨을 것입니다. 트리 쉐이킹은 특히 사용하지 않는 내보내기 제거와 관련된 데드 코드 제거 의 한 형태입니다. 모든 모듈을 연결하면 사용하지 않는 내보내기는 사실상 데드 코드이므로 제거할 수 있습니다. 그러나 미사용 내보내기를 결정하는 프로세스는 간단하지 않습니다. Tree Shaking은 일반적으로 자바스크립트 엔진(예: V8 또는 Hermes)이 아닌 컴파일러/번들러 수준(예: Webpack 또는 ESBuild )에서 구현됩니다. JavaScript의 많은 패턴이 트리 쉐이킹을 깨뜨릴 수 있지만 이 기사에서는 한 가지 측면인 모듈 시스템에 집중하고 싶습니다. 여기서 이해해야 할 두 가지 관련 모듈 시스템은 CommonJS 모듈 과ES 모듈 .

module.exports = {}CommonJS는 또는 를 작성할 때 사용됩니다 exports.someMethod = () => {}. ES 모듈은 importexport구문으로 식별됩니다. 컴파일러가 ES 모듈보다 CommonJS를 사용하는 코드에 Tree Shaking을 적용하는 것이 더 어렵습니다. CommonJS 모듈은 종종 동적이며 ES 모듈은 정적으로 분석할 수 있습니다. 예를 들어 다음 코드에서 모든 내보내기 식별자를 정적으로 감지하는 것은 쉬운 일이 아닙니다.

ES 모듈은 설계상 정적으로 분석 가능하므로 컴파일러는 사용하지 않는 내보내기를 더 쉽게 감지할 수 있습니다.
따라서 CommonJS 모듈이 아닌 최적화 컴파일러 ES 모듈이 작동하도록 하는 것이 가장 좋습니다.

배경

Klarna에 합류하기 전에는 React Native 작업 경험이 없었습니다. 일상적인 리팩토링 중에 다음 diff를 적용했습니다.

사용된 번들러 가 false someFeatureMethod인 경우 사용되지 않은 것으로 간주 하여 최종 번들에서 제거 한다고 가정합니다 . 코드 검토 중에 이 diff는 문제가 있는 것으로 표시되었으므로 나는 앉아서 내 가정과 이것이 어디에서 깨졌는지 다시 확인했습니다. 운 좋게도 우리는 이미 몇 달 전에 Webpack( Re.Pack 형식)으로 전환 하여 . 이렇게 하면 최종 JavaScript 번들을 검사할 수 있는 방식으로 번들링 프로세스를 구성하기가 더 쉬워집니다. 최종 JavaScript 출력을 보려면 Hermes 만 비활성화해야 했습니다.SOME_STATIC_FLAGsome-feature-moduleReact.lazy

가져온 위치를 쉽게 찾을 수 있도록 약간의 시행 착오를 거친 후 some-feature-module다음 줄을 발견했습니다 c=(n(463526),n(456189). 쉼표 연산자 는 일반적으로 사용하지 않는 것이므로 이것이 수행하는 작업을 요약하겠습니다. 모든 피연산자를 평가하고 마지막 피연산자. 즉, 의 반환 값 n(463526)이 사용되지 않았습니다. 나는 이미 웹에서 트리 쉐이킹 작업을 해 본 경험이 있었기 때문에 축소하기 전에 이것이 무엇인지 꽤 분명했습니다. require('some-feature-module')(Webpack은 가져오기 소스 문자열을 숫자로 변환합니다.)

Webpack은 실제로 someFeatureMethod사용되지 않는 것을 인식하여 사용을 제거했습니다. 그러나 Webpack은 모듈에서 사용하지 않는 내보내기를 제거하지 않고 모듈에 부작용이 있는지 알지 못하기 때문에 가져오기를 유지했습니다. 모듈에 부작용이 있는 경우 프로그램 흐름이 변경될 수 있으므로 번들에서 모듈을 제거할 수 없습니다.

원래 diff가 예상대로 작동하도록 하기 위해 우리가 해야 할 일은 Tree Shaking이 최종 번들에 적용되는지 확인하는 것뿐이었습니다.

구현

Webpack이 모든 모듈을 번들로 묶기 전에 ES 모듈을 CommonJS로 변환하지 않도록 하는 것이 모두 중요합니다. Metro Babel 사전 설정 (새로운 React Native 앱의 기본값)을 사용하는 경우 대부분의 작업은 다음을 활성화하는 것으로 귀결됩니다 disableImportExportTransform.

이 옵션은 현재 문서화되지 않았으며 언제든지 제거될 수 있습니다.

또한 CommonJS 모듈 대신 ES 모듈을 사용하는 진입점을 사용하도록 Webpack에 지시해야 했습니다. 개별 파일 의 경우 패키지의 경우 기본 필드.mjs 를 사용하도록 Webpack에 지시해야 했습니다 .module

그러나 이것은 우리가 JavaScript를 작성하는 방법과 React Native 생태계에서 코드를 작성하는 방법에 대한 문제를 드러냈습니다. 3가지 유형의 문제를 확인했습니다.

다른 구문 내보내기 mainmodule

main이러한 기본 필드는 모듈 시스템( CommonJS의 module경우 ES 모듈의 경우) 을 구분하는 데에만 사용해야 합니다 . module그러나 많은 패키지는 진입점 에서 더 현대적인 구문을 제공합니다 . 예를 들어 class구문은 현재 Hermes에서 지원되지 않습니다 .

지금 은 Webpack 구성에 사용자 지정을 추가하여 모든 node_modules콘텐츠를 ES5 구문 또는 Hermes에서 지원하는 구문으로 변환합니다.rule

모호한 구문으로 CommonJS 모듈 가져오기

Webpack은 모듈 시스템을 혼합하는 모듈에서 내보내기를 찾을 수 없습니다. 그러나 React Native 자체는 소스 파일을 혼합 모듈 시스템과 함께 제공합니다.

rule여기서 해결책 은 Webpack 구성 에 특수 항목을 추가하여 이러한 모듈을 CommonJS로 계속 변환하는 것입니다(따라서 트리 흔들림 비활성화) .

존재하지 않는 가져오기 식별자

이것은 실제로 대부분이 인식하지 못하는 JavaScript의 SyntaxError입니다. 예를 import { doesNotExist } from 'some-module';들어 SyntaxError. 이는 대체로 개발자에게는 성가신 일이지만 실제 런타임 문제로 이어질 수 있습니다. 우리는 Webpack 구성에서 활성화하여 Webpack에서 ES 모듈의 엄격한 구현을 시행했습니다 module.parser.javascript.exportsPresence.

TypeScript에서 유형을 다시 내보냄으로써 발생하는 이러한 문제의 대부분은 예를 들어

다행히 TypeScript는 다음을 활성화하여 유형 수준에서 이러한 문제에 플래그를 지정할 수 있습니다 isolatedModules.

type가져오기 이름에 대한 수정자는 TypeScript 4.5의 새로운 기능입니다. type가져오기 이름에 대한 수정자에 대한 지원을 추가하는 것은 우리가 사용한 ESLint 파서 , Prettier 및 TypeScript를 업그레이드해야 했기 때문에 상당한 도전이었습니다.

가져오기 이름에 수정자를 추가하면 typeBabel이 런타임에 실제로 존재하지 않는 유형 가져오기를 제거하게 됩니다.

결과

초기 구현은 꽤 해키했습니다. 그러나 첫 번째 결과는 이미 두 플랫폼 모두에서 중앙값 20%의 시작 개선을 보여주었습니다(Samsung Galaxy S9 2.2s는 2.8s에서, iPhone 11은 802ms에서 640ms 감소).

우리가 본 것은 초기의 중요한 JavaScript 청크의 46% 감소였습니다. 우리가 출하한 JavaScript의 전체 크기는 14% 감소했습니다. 그 차이는 주로 메인 청크에서 비동기 청크(기능 및 경로)로 코드를 이동했기 때문입니다.

나무 흔들기 후 번들 통계의 절대 차이
트리 쉐이킹 후 번들 통계의 상대적 차이

이러한 이미지는 놀라운 statoscope.tech 에 의해 생성되어 이 변경 사항을 분석하는 데 도움이 되었으며 번들 크기를 더욱 개선하는 데 계속 도움이 될 것입니다.

감소는 사용하지 않는 내보내기를 제거하는 데서 오는 것이 아니라 Webpack이 ModuleConcatenationPlugin더 많은 모듈을 연결할 수 있다는 점에 유의하십시오. 즉, 더 많은 모듈을 호이스트 할 수 있습니다 . 우리는 아직 스코프 호이스팅을 완전히 활용하고 있지 않습니다. 지금은 모듈의 20%만 호이스팅됩니다. 그 수를 늘리면 더 많은 번들 크기와 런타임 이득을 기대했습니다.

JavaScript의 40% 감소는 JavaScript 번들을 실행하기 전에 평가하는 데 걸리는 시간과 거의 정확히 1:1로 매핑됩니다. JavaScript를 평가하면 결과 시작 시간이 차단되므로 직접 배송되는 JavaScript의 양을 줄이면 시작 시간이 줄어듭니다.

구현의 마지막 단계가 2주 후에 완료된 후에도 우리는 여전히 랩에서 동일한 결과를 얻었고 이 기능을 메인 브랜치에 적용할 준비가 되었습니다. 릴리스 컷오프 직후 최종 변경 사항을 적용하기 위해 각별한 주의를 기울였습니다. 이를 통해 직원들이 사용하는 내부 앱 버전에서 새로운 모듈 시스템을 광범위하게 테스트할 수 있었습니다. 1주일 간의 내부 테스트 후 이 기능은 최종 사용자에게 점진적으로 출시되었습니다. 우리는 앱 안정성이 크게 영향을 받지 않는다는 것을 확인한 후 매우 희망적이었습니다. 생산 데이터는 랩 결과에서 본 시작 시간 중앙값과 동일한 상대적 개선을 보여주었습니다.

버전 22.37은 트리 흔들림이 없습니다. 22시 38분 나무 흔들기

Android 사용자를 위한 프로덕션 데이터
iOS 사용자를 위한 프로덕션 데이터

이러한 개선 사항으로 인해 빌드 시간이 늘어납니다. 프로덕션 JavaScript 번들을 번들로 묶는 데 약 30%(4분) 더 많은 시간이 걸립니다. 우리는 이러한 증가된 빌드 시간이 더 나은 사용자 경험으로 직접 변환되기 때문에 기꺼이 받아들입니다. 증가된 빌드 시간 중 일부는 필요한 것보다 더 많이 트랜스파일했기 때문입니다. 초기 구현에서는 트랜스파일에 필요한 양을 줄이는 데 시간을 할애하지 않았습니다. 또한 더 많은 패키지가 적절한 ES 모듈을 제공할수록 빌드 시간 증가의 일부를 보상할 것입니다. JavaScript 번들링 시간은 React Native 앱을 빌드하는 데 필요한 유일한 작업이 아닙니다. 바이너리 컴파일 등으로 인해 JavaScript 번들링의 증가는 결국 그다지 영향을 미치지 않습니다.

무엇 향후 계획

React Native 생태계에서 ES 모듈이 활발하게 작업되지 않은 것 같습니다. module우리는 ES 모듈의 적절한 사용(예: 동등한 구문으로 JavaScript를 가리키는 항목) 에 대해 생태계를 더 정렬하기를 원할 것 입니다. 그렇게 하면 빌드 구성을 줄이고 트랜스파일을 줄일 수 있습니다.

Metro( )의 플래그 뒤에 ES 모듈 사용에 대한 지원이 있지만 experimentalImportSupport실험적인 것으로 표시되고 문서화되지 않았습니다. 개발 단계에서 해당 플래그를 활성화하는 것은 (아직) 우리에게 효과가 없지만 언젠가는 개발 및 생산 단계에서 동일한 모듈 시스템을 사용할 수 있기를 바랍니다. ES 모듈에 대한 지원이 현재 활발히 진행되지 않는 것 같기 때문에 React Native에서 ES 모듈에 대한 논의를 다시 시작하고 싶습니다. Tree Shaking 지원도 몇 년 전에 완전히 중단되었습니다. .

결국 ES 모듈은 JavaScript를 아는 모든 사람이 결국 배우는 언어 기능입니다. 번들 분할 및 데드 코드 제거를 이해하기 위해 React Native에 추가 학습 단계가 있어야 할 이유가 없습니다.

한 번 쓰고 어디든 달려가세요!

이 게시물이 마음에 드셨나요? MediumLinkedIn 에서 Klarna Engineering을 팔로우하여 이와 같은 더 많은 기사를 계속 확인하십시오.