Vanilla-extract만든 design system의 번들 사이즈가 너무 커요!
CSS-in-TS 와 zero-runtime css 컨셉을 채택한 vanilla-extract css(이하 VE로 줄임!)로 NPM 라이브러리를 만들어 배포하였다.
사내 디자인 시스템을 만든 작업이었다.
VE는 제로 런타임을 위해서 빌드 타임에 코드로 정의한 css와 타입정의를 뽑아주게 된다. 이 css와 타입을 라이브러리에 같이 shipping하도록 제작했다.
처음 라이브러리를 만들때에는 없는것보단 낫겠지 하는 심정으로 이것저것 필요해보이는 디자인 토큰과 기능들을 넣었다.
하지만 결론적으로 이런 접근 방식으로 큰 코를 다치게 되는데...
설정 살펴보기
어느날 빌드를 하다가 바벨의 불평을 마주치게 되었다.
\[BABEL\] NOTE: the code generator has deoptimised the styling of .../sprinkles.vanilla.css.js as it exceeds the max of 500kb.
이 주의문으로부터 시작되어 라이브러리의 번들 관련한 문제점이 없는지 설정부터 빌드까지 싹 검토해보게 되었다.
Rollup Bundle Analyzer 설치
Rollup으로 개발한 라이브러리였으므로 rollup-plugin-analyzer
플러그인을 설정하여 모듈 분석을 실행할 수 있다.
// rollup.config.mjs
import analyze from "rollup-plugin-analyzer";
const plugins = [
analyze({
limit: 5, // output top 5 largest files
})
// others...
];
export default [
{
input,
plugins, // here
external,
onwarn,
},
// Other configs...
];
이렇게 설정해주면, 빌드 시 가장 크게 번들을 차지하는 상위 5가지 모듈을 터미널에서 볼 수 있다.
1차 경량화 14% ⬇️ (external packages)
번들에 포함되지 않을 패키지들을 외부 패키지로 취급하도록 설정을 수정하였고, 14% 번들사이즈를 감량할 수 있었다.
📦 ESM: 1.465MB --> 1.255 MB (0.21MB = 14.3% ⬇️)
📦 CJS: 1.474 MB --> 1.268 MB (0.206MB = 14.0% ⬇️)
빌드한 상태에서 dist 폴더를 살펴보니, node_modules가 존재한 것을 발견했다. (clsx, date-fns 등)
이 의미는 외부 패키지들이 제대로 트리쉐이킹이 되지 않아 불필요한 외부 패키지가 그대로 라이브러리의 번들에 포함되고 있다는 의미이다. 불필요한 패키지들이 그대로 번들에 포함되고 있어 외부 디펜던시를 명확히 구분해주어 필요한 코드만 shipping할 수 있게 수정이 필요하다.
내가 수정한 부분은 다음과 같다.
1. package.json에서 react와 마찬가지로 react-dom도 peer dependencies로 이동
현재 개발하는 라이브러리는 react, react-dom은 라이브러리를 사용하는 프로젝트에 이미 설치되어 있을 것을 가정하고 있다.
따라서 이 라이브러리를 직접 설치하지 않고, 프로젝트에 설치된 패키지에 의존하여 동작되도록 peerDependencies로 이동할 수 있다.
2. 외부 모듈로 취급되어야 하는 모듈들을 dependencies로 이동시키고, dependencies와 peerDependencies를 external 옵션으로 설정.
Rollup external 옵션이 존재한다.
여기에 존재하는 모듈들은 외부 라이브러리로 취급할 수 있다. 번들에서 제외해야 하는 모듈 이름을 작성하면 모듈에서 제외할 수 있다.
아래와 같이 package.json에서 dependencies, peerDependencies에 지정된 모듈 이름을 가져와 external 옵션으로 설정할 수 있다.
// rollup.config.mjs
import packageJson from "./package.json" assert { type: "json" };
/**
* Used for generating external dependencies
* Credit: Mateusz Burzyński (https://github.com/Andarist)
* Source: https://github.com/rollup/rollup-plugin-babel/issues/148#issuecomment-399696316
*/
const makeExternalPredicate = (externalArr) => {
if (externalArr.length === 0) {
return () => false;
}
const pattern = new RegExp(`^(${externalArr.join("|")})($|/)`);
return (id) => pattern.test(id);
};
const external = makeExternalPredicate([
// Handles both dependencies and peer dependencies so we don't have to manually maintain a list
...Object.keys(packageJson.dependencies || {}),
...Object.keys(packageJson.peerDependencies || {}),
]);
export default [
{
input,
plugins,
external, // here
onwarn,
},
// Other configs...
];
Ref: https://github.com/rollup/rollup-plugin-babel/issues/148#issuecomment-399696316
결과
이렇게 설정한 후, 재빌드하여 Dist 폴더를 살펴보면 설정한 외부 모듈들이 모두 제거된 것을 확인할 수 있다.
하지만 node_modules
에 여전히 tslib
, style-inject
폴더만은 사라지지 않고 남아있었다.
tslib
는 타입스크립트 변환 과정과 연관이 있었다.
Type safety를 제공하는 라이브러리를 만들기 위해 @rollup/plugin-typescript
플러그인을 사용하고 있었는데, rollup이 타입스크립트를 직접 컴파일할 수 있게 하는 플러그인으로 빌드 과정에서 타입스크립트를 자바스크립트로 컴파일하고 타입 검사와 변환을 수행한다.
그리고 타입스크립트에서 ES6 이상의 기능을 사용할때에는 내부적으로 tslib 라이브러리를 사용하는데, 이는 ES6 이상의 구문(async/await, extends 등)을 변환할 때 공통적으로 필요한 헬퍼 함수들이 많기 때문이다.
tslib을 사용함으로서 es6 이상 구문을 중복하지 않고 최적화된 번들을 생성할 수 있다고 한다.
style-inject
는 rollup-plugin-postcss
플러그인과 연관이 있었다.
postcss를 사용하여 CSS 파일을 js 파일로 변환하고, 브라우저 환경에서 동적으로 삽입하기 위해 style-inject
라이브러리를 사용한다고 한다. 브라우저가 실행될 때, 이 스타일을 style 태그로 동적으로 head 태그에 삽입한다.
그러면 CSS 파일을 별도로 로드할 필요없이, 이 CSS가 필요한 컴포넌트 번들에 이 스타일이 포함되게 된다. 불필요한 스타일을 로드하지않아 페이지 로딩 속도를 최적화할 수 있고, 여러 리소스를 로드할필요가 없어 네트워크 성능에도 효율적인 방식이다. 다만, js 번들 크기가 커질수있는 단점이 존재한다.
2차 경량 5% (esbuild minify 옵션)
📦 ESM: 1.255 MB --> 1.182 MB (0.073MB = 5.82% ⬇️)
📦 CJS: 1.268 MB --> 1.195 MB (0.073MB = 5.76% ⬇️)
rollup-plugin-esbuild 플러그인을 사용하여 esbuild minify 옵션을 설정하고 CSS의 whitespace, 주석 등을 제거하고 단축 구문으로 최적화하여 파일 축소해보았다.
제로 런타임을 위한 css 파일 수가 많아 시도해본 전략인데, 사실상 큰 감량을 해주지 않고있어 번거롭게 코드 관리포인트만 늘어날 것 같아 삭제했다.
3차 경량 50% (Sprinkles 축소)
사실상 이게 가장 크다.
VE가 제공하는 sprinkles()을 적극 활용하여 sx prop을 설계하여 제공하고 있었는데, 여기에서 불필요한 조합을 대폭 삭제하여 실질적으로 sprinkles()가 만들어내는 조합의 수를 줄여 번들 사이즈를 절반가랑 줄인 방법이다.
📦 ESM: 1.182 MB --> 575.853 KB (606.147 KB = 51.28% ⬇️)
📦 CJS: 1.195 MB --> 588.822 KB (606.178 KB = 50.73% ⬇️)
사실 내가 생각해도 VE를 이렇게 쓰는게 맞나, 싶어서 vanilla-extract-css discussion에 예시를 정리하여 사례와 질문을 올렸다.
요약해보자면,
1. spinkles()을 통해 여러 디자인 토큰을 제공하고 있다.
2. 디자인 토큰을 기반한 여러 조건(반응형, 다크/브라이트 모드, 가상 클래스)을 제공했다. 그런데 빌드된 결과물을 살펴보니 조합의 수만큼 생성되는 가짓수가 폭발하고 있다. 번들 사이즈도 같이 폭발..💥
3. 사실상 가상 클래스가 잘 쓰이지 않아 삭제하려하지만, 이 접근 방식이 좋은 케이스가 아닌 것같다.
4. 내 생각에는 이렇게 쓰는게 아닌것같아 "이거 안티패턴인가요" 했으나,
5. 다른 분의 답변 "그런거같아"...
여담
설정을 뒤져보며 빌드를 여러 차례 하다보니 빌드가 너무 느려 속이 터지기 시작했다.
@rollup/plugin-terser 플러그인의 옵션 maxWorkers를 사용하면 빠르게 빌드할수있다.
'Web Development' 카테고리의 다른 글
🚧 Nestjs 에러 로깅 (0) | 2024.11.26 |
---|---|
어? 이거 설치한적없는데? 👻 패키지 매니저 알아보기 - npm, yarn, yarn berry, pnpm (2) | 2024.11.24 |
Next.js v12.0 점진적으로 개편하기(Feat. 도커이미지 95% 경량화) (0) | 2024.10.27 |
퇴사 부검 ⚰️ (4) | 2024.09.30 |
Issue 템플릿 작성하기(라벨, 할당자, 타이틀 자동 설정, 오픈소스 참고) (0) | 2024.08.29 |