[webpack] 번들 사이즈 최적화

Gyuhan Park·2024년 9월 10일
1

Tech

목록 보기
3/3

[ 요약 ]
webpack 5 production build 최적화 기본 적용
webpack-bundle-analyzer 로 번들 사이즈 분석
mini-css-extract-plugin : JS 파일에서 css 파일 분리
css-minimizer-webpack-plugin : css 파일 압축 (minify) - 번들 사이즈에는 영향 안줌
코드 스플리팅 : 초기 로딩에 불필요한 코드 분리
번들 사이즈 개선 : 65.24KB → 55.29KB (15.25% 감소)
홈화면 번들 글자수(공백 줄바꿈 포함) : 218,472 → 168,432

💭 TMI

사용자가 우리 서비스를 이용하려면 자바스크립트 파일을 네트워크로 가져와 화면을 그린다. React로 만든 애플리케이션은 SPA 이기 때문에 자바스크립트를 불러오기 전까지 HTML이 비어있다. 따라서 사용자는 자바스크립트가 불러와지기 전까지 빈 화면을 보게 된다.

자바스크립트 번들 사이즈를 줄임으로써 네트워크 요청 비용을 줄이고 초기 로딩 속도를 높이려고 한다.

🔍 번들 사이즈 분석 (개선 전)

얼마나 개선되었는지 파악하기 위해 webpack-bundle-analyzer 로 번들 사이즈를 분석하였다. 배포했을 때 기준 보통 gzip 형식으로 압축되기 때문에 gzip 압축 사이즈를 기준으로 확인하였다.

npm i -D webpack-bundle-analyzer
// webpack.config.js
const path = require('path');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  ...
  devtool: 'source-map',
  plugins: [
    ...
    new BundleAnalyzerPlugin(),
  ],
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/i,
        exclude: /node_modules/,
        use: {
          loader: 'ts-loader'
        }
      },
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
        loader: 'file-loader',
        options: {
          name: 'static/[name].[ext]'
        }
      }
    ]
  },
  optimization: {
    minimizer: false
  }
};

✅ development build

dev build : 474.5KB
bundle.js 글자수(공백 줄바꿈 포함) : 2,230,572

✅ production build (minimize: false)

production build (minimize : false) : 242.77KB
bundle.js 글자수(공백 줄바꿈 포함) : 970,349

✅ production build

production build : 65.24KB
bundle.js 글자수(공백 줄바꿈 포함) : 218,472

🔍 개선 과정

✅ mini-css-extract-plugin

CSS 파일 추출하는 플러그인
CSS를 포함하는 JS파일마다 css 파일 생성
mini-css-extract-plugin 은 css-loader와 결합하는 것을 권장
65.24KB → 58.18KB

npm i -D mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  ...,
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css'
    }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      },
    ]
  },
};

✅ css-minimizer-webpack-plugin

css 파일 minify 하는 플러그인

로컬 기준 main css 파일 : 12.4KB → 10.0KB

npm i -D css-minimizer-webpack-plugin
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  ...,
  optimization: {
    minimizer: [new CssMinimizerPlugin(), '...']
  }
};

✅ source-map 제거

production build 시 source-map은 필요없다고 판단
58.22KB → 58.18KB

// ❌ before
devtools: 'source-map'

// ✅ after
devtool: isProduction ? false : 'source-map',

✅ 코드 스플리팅 적용

홈 화면에서 불필요한 검색 화면 코드를 코드 스플리팅 적용
58.18KB → 55.29KB

🔍 번들 사이즈 분석 (개선 후)

production build : 65.24KB → 55.29KB
bundle.js 글자수(공백 줄바꿈 포함) : 218,472 → 168,432

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const Dotenv = require('dotenv-webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

const isProduction = process.env.NODE_ENV === 'production';

module.exports = {
  entry: './src/index.tsx',
  resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] },
  output: {
    filename: '[name].[contenthash].js',
    path: path.join(__dirname, '/dist'),
    clean: true
  },
  devServer: {
    hot: true,
    open: true,
    historyApiFallback: true
  },
  devtool: isProduction ? false : 'source-map',
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html'
    }),
    new CopyWebpackPlugin({
      patterns: [{ from: './public', to: './public' }]
    }),
    new Dotenv(),
    new BundleAnalyzerPlugin(),
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css'
    })
  ],
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/i,
        exclude: /node_modules/,
        use: {
          loader: 'ts-loader'
        }
      },
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      },
      {
        test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
        loader: 'file-loader',
        options: {
          name: 'static/[name].[ext]'
        }
      }
    ]
  },
  optimization: {
    minimizer: [new CssMinimizerPlugin(), '...']
  }
};

🔥 webpack v5 production build 최적화 기본값 deepdive

webpack 공식문서의 mode 부분을 보면 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, TerserPlugin 으로 총 5가지만 기본 적용되는 것으로 이해했다.
하지만 다른 블로그들을 보면 대부분 7가지가 적용된다고 하길래 실제 소스코드를 들여다보게 되었다.

webpack 실제 소스코드의 1413번째 줄을 보면 최적화 기본값을 적용하는 함수를 확인할 수 있다.

const applyOptimizationDefaults = (
	optimization,
	{ production, development, css, records }
) => {
	D(optimization, "removeAvailableModules", false);
	D(optimization, "removeEmptyChunks", true);
	D(optimization, "mergeDuplicateChunks", true);
	D(optimization, "flagIncludedChunks", production);
	F(optimization, "moduleIds", () => {
		if (production) return "deterministic";
		if (development) return "named";
		return "natural";
	});
	F(optimization, "chunkIds", () => {
		if (production) return "deterministic";
		if (development) return "named";
		return "natural";
	});
	F(optimization, "sideEffects", () => (production ? true : "flag"));
	D(optimization, "providedExports", true);
	D(optimization, "usedExports", production);
	D(optimization, "innerGraph", production);
	D(optimization, "mangleExports", production);
	D(optimization, "concatenateModules", production);
	D(optimization, "runtimeChunk", false);
	D(optimization, "emitOnErrors", !production);
	D(optimization, "checkWasmTypes", production);
	D(optimization, "mangleWasmImports", false);
	D(optimization, "portableRecords", records);
	D(optimization, "realContentHash", production);
	D(optimization, "minimize", production);
    ...

소스코드를 확인한 결과 총 9가지가 production mode 일 때만 true를 반환하고 있었다.
https://webpack.kr/configuration/optimization

  • flagIncludedChunks -> 더 큰 청크가 이미 로드된 경우, 다른 청크의 하위집합인 청크를 확인하고 하위집합을 로드하지 않는 방식.
  • sideEffects -> package.json의 sideEffects 필드 인식.
  • usedExports -> 사용하지 않은 export 제거
  • innerGraph -> 사용되지 않은 export에 대해 내부 그래프 분석을 수행할지 결정
  • mangleExports -> export 맹글링을 제어
  • concatenateModules -> 단일 모듈로 안전하게 연결할 수 있는 모듈 그래프의 세그먼트를 찾음
  • checkWasmTypes -> WebAssembly 모듈을 가져오거나 내보낼 때 호환되지 않는 유형의 WebAssembly 모듈을 확인하도록 webpack에 지시
  • realContentHash -> 애셋이 처리된 후 올바른 애셋 콘텐츠 해시를 얻기 위해 추가 해시 컴파일 패스를 추가
  • minimize -> 번들 최소화. TerserPlugin

cf) 알고보니 webpack v4에서 v5로 마이그레이션 되면서 없어진 속성들이다. webpack v4에는 7가지로 소개되어있는데, webpack v5에는 5가지로 수정되었다.

  • OccurrenceOrderPlugin
  • SideEffectsFlagPlugin
profile
단단한 프론트엔드 개발자가 되고 싶은

0개의 댓글