Webpack에서 Tree Shaking 적용하기
egjs의 InfiniteGrid 컴퍼넌트에 용량 개선 작업 중에 Tree Shaking을 도입하면서 알게된 사실을 정리해봤습니다.
환경은 webpack 3.x, 4.x | babel-loader 8.x | babel 7.x에서 테스트했습니다.
npm install babel-loader @babel/core @babel/preset-env webpack
Tree Shaking?
Tree Shaking은 사용하지 않는 코드를 제거함으로써 용량을 줄일 수 있는 방식입니다.
특정 라이브러리를 참조하면 참조한 라이브러리의 크기만큼 최종 번들의 용량이 증가하게 됩니다. 하지만 특정 라이브러리의 모든 코드를 쓰지 않습니다.
- 그림과 같이 APP은 C의 2번만을 사용하고 있습니다. 그렇다면 C의 1번과 3번을 코드에 포함할 필요가 있을까요?
- 또한 C의 2번은 B의 3번, B의 3번은 A의 3번을 참조하고 있습니다. 그렇다면 B의 1번, 2번 A의 1번, 2번은 코드에 포함할 필요가 있을까요?
Tree Shaking은 Bundler(webpack, rollup)와 UglifyJS가 역할을 담당 하고 있습니다.
- Bundler의 역할은 사용하지 않는 모듈에 사용하지 않았다는 주석과 함께 export를 제거합니다.
(rollup과 webpack4(sideEffects 활성화된 경우)에서는 사용하지 않는 모듈을 직접 제거합니다.) - UglifyJS는 Bundler를 통해 export가 제거된 경우 모듈은 dead code가 되어 제거됩니다.
리액트 컴퍼넌트에서 InfiniteGrid 사용해서 빌드한 사진입니다.
- Tree Shaking 적용 전
- Tree Shaking 적용 후
Tree Shaking을 적용해보면 좋을 것 같은 대상
모듈이 분리 되어 있고 사용자는 특정 모듈만 사용하는 경우
- InfiniteGrid 같은 경우 5가지 layout을 제공하고 있고 사용자는 그 중 1개만 사용하기 때문에 다른 4개를 사용할 필요가 없습니다.
- react-infinitegrid는 InfiniteGrid의 코어(InfiniteGrid의 2/5 크기)를 사용하지 않습니다.
- view360경우에도 2개 뷰어중 1개만 사용할 수 있고 Axes도 마찬가지로 4개의 Input을 사용하고 1개만 사용하는 경우라면 3개를 사용하지 않게 됩니다.
Tree Shaking을 어떻게 적용할까?
라이브러리를 제공하는 쪽에서 설정을 해야 합니다. (단, 사용자의 bundler에 따라 결과가 다를 수 있습니다.)
require가 아닌 import / module.export(s)가 아닌 export default와 export로 이루어져 있어야 합니다.
the static structure of ES2015 module syntax, i.e. import and export
package.json에 module필드를 사용합니다.
- webpack을 사용하는 경우 import(또는 require)로 참조하면 module을 우선순위로 갖고 다음 main을 접근합니다.
https://webpack.js.org/configuration/resolve/#resolve-mainfields - require는 export된 모든 모듈을 접근하기 때문에 Tree Shaking이 되지 않습니다.
"main": "dist/infinitegrid.js",
"module": "dist/esm/",
.babelrc preset에 “modules”: false로 지정합니다.
modules를 false로 하면 import, export은 require, module.exports로 바뀌지 않습니다. (Tree Shaking 조건)
- .babelrc 적용전
{
“presets”: [
[
“@babel/preset-env”,
{
“loose”: true
}
]
]
}
- .babelrc 적용후
{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"loose": true
}
]
]
}
Tree Shaking이 적용되지 않는 경우
사용하지 않더라도 코드에 포함되는 경우가 있습니다. 사용하지 않지만 다른 코드에 영향을 끼칠 수 있다고 판단하는데 이런 경우를 Side Effect가 발생했다고 합니다.
- 전역함수를 사용하는 경우 (Object, Math, String, RegExp)
export const isMobile = /mobi|ios|android/.test(agent);
2. 함수 실행 코드에서 멤버변수를 변경하고 반환하는 경우
3. static class properties를 사용하는 경우 (babel)
https://babeljs.io/docs/en/next/babel-plugin-proposal-class-properties.html
static method는 side effect가 발생하지 않습니다.
그래서 babel플러그인을 직접 만들어 해결했습니다. (babel-plugin-no-side-effect-class-properties)
4. Class를 사용하는 경우 (babel < 7)
babel 7 부터는 class를 babel로 변환시 /*#__PURE__*/라는 라벨이 붙습니다.
/*#__PURE__*/ 라벨이 붙으면 Side Effect가 일어나지 않는 영역으로 간주되기 때문에 prototype을 사용하더라도 Side Effect가 발생하지 않습니다.
https://babeljs.io/blog/2017/09/12/planning-for-7.0#pure-annotation-in-specific-transforms-for-minifiers
babel 7 이전 버전에서는 플러그인으로 지원이 가능합니다.
https://www.npmjs.com/package/babel-plugin-annotate-pure-calls
5. import한 라이브러리를 사용한 함수를 사용하지 않는 경우(webpack 3)
- b를 사용하지 않으면 b는 제거되더라도 b안에 a가 있기 때문에 a는 side effect가 뜨면서 제거되지 않습니다.
6. import한 라이브러리를 다시 export default …한 경우(webpack 3)
- export default할 경우 사용하지 않더라도 참조되는 코드가 추가됩니다.
- export {… as default}로 바꾸면 가능합니다.
Side Effect가 일어나는 코드지만 발생하지 않게 하는 방법
Side Effect가 발생하지만 실제로는 영향을 끼치지 않는 코드일 수 있습니다.
- 다른 코드 또는 외부에 영향을 끼치지 않는다고 자신할 수 있는 코드라면 side effect가 없다고 package.json에 명시해줍니다. (webpack4)
{
// side effect가 전혀 없는 경우
"sideEffects": false,
// side effect가 일부만 있는 경우
"sideEffects": ["./dist/....", "./dist/...."]
}
또한 /*#__PURE__*/라벨을 직접 붙이는 방법이 있습니다.
- /*#__PURE__*/라벨을 붙이면 Side Effect가 일어나지 않는다고 판단합니다.
강제로 Side Effect 발생하는 방법
VERSION과 같이 무조건 포함되어야 하는 요소들은 사용하지 않더라도 있어야 합니다.
- 파일을 만들어 export default 대신 module.exports을 사용하여 Tree Shaking이 되지 않게 합니다.
module.exports = "#__VERSION__#";
typescript환경에서 적용하는 방법
typescript는 tsc로 transpile하면 Class는 /** @class */라는 라벨이 붙습니다.
UglifyJS는 /** @class */라벨을 지원하지 않기 때문에 /*#__PURE__*/라벨을 수동으로 붙혀줘야 합니다.
- replace-in-file 모듈을 써서 라벨을 바꾸는 스크립트입니다.
"scripts": {
"replace": "replace-in-file '/** @class */' '/*#__PURE__*/' dist/*/*.js"
}
$ npm run replace
결과
13개의 모듈 중 크기가 작은 모듈 1개만 사용해서 빌드를 했습니다.
import {GridLayout} from “@egjs/infinitegrid”;GridLayout();
- es3 형식의 bundle을 가리켰을 경우 tree shaking이 적용되지 않기 떄문에 bundle의 용량만큼 늘어납니다.
- webpack3을 사용하여 es모듈을 가르킨 경우 전혀 참조 되지 않는 모듈만 제거됩니다.
- webpack3에서 es모듈 중 import하고 export default된 경우 export {default as …}로 바꾸면 Tree Shaking이 적용됩니다.
- webpack4에서는 참조되더라도 사용하지 않으면 전부 제거됩니다. 하지만 side effect가 일어나는 것은 제거가 안됩니다.
- webpack4를 사용하고 package.json에서 “sideEffects”: false를 설정하면 side effect가 일어나도 사용하지 않으면 제거됩니다.
위의 스크린샷 처럼 Tree Shaking 을 통해 사용하지 않는 코드를 제거함으로써 51kb 에서 4kb 까지 용량을 줄일 수 있었습니다. 저희의 경험이 웹팩 환경에서 용량 절감을 위해 고민하고 계신 분들께도 조금이라도 참고가 되셨으면 좋겠습니다.