[ 요약 ]
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
사용자가 우리 서비스를 이용하려면 자바스크립트 파일을 네트워크로 가져와 화면을 그린다. 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
}
};
dev build : 474.5KB
bundle.js 글자수(공백 줄바꿈 포함) : 2,230,572
production build (minimize : false) : 242.77KB
bundle.js 글자수(공백 줄바꿈 포함) : 970,349
production build : 65.24KB
bundle.js 글자수(공백 줄바꿈 포함) : 218,472
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']
},
]
},
};
로컬 기준 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(), '...']
}
};
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 공식문서의 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