Всем привет! Один из проектов на работе у нас изначально создан на create-react-app утилите (кстати у меня есть статья по поводу того, что сейчас происходит с CRA и что его ждет в будущем). Встал вопрос по поводу того, можно ли как-то оптимизировать сборку по скорости и весу сжатого проекта, так как есть большие планы на рост проекта и не хотелось бы, чтобы что-то начало тормозить, и этим соответственно я и занялся. Хочу рассказать о том, как все проходило, какие шаги были пройдены и что в итоге получилось. Также в конце приложу код всей конфигурации.
Сразу хотел бы сказать, что это очередная статья по вебпаку с приведенной его настройкой и некоторые могут не увидеть ничего нового, так как тут будет описана последовательность выполненных действий и настроек. Прошу не кидать камни, так как я просто хочу поделиться опытом, возможно даже получить советы по лучшей настройке и, надеюсь, кому-то это все-таки поможет.
Пора эджектить
Первым делом, конечно же, был выполнен eject проекта. После чего я получил кучу папочек и файлов, которые лежали под капотом CRA.
Чтобы отслеживать скорость сборки и сжатый вес я сразу поставил Statoscope в проект. Ставится он очень просто. Устанавливается плагин в проект, импортируется в файл вебпака и вносится в список плагинов. Его всячески можно настраивать, но в данном случае мне не было это нужно.
npm i --save-dev @statoscope/webpack-plugin
...
const StatoscopeWebpackPlugin = require('@statoscope/webpack-plugin').default;
module.exports = {
...
plugins: [
...
new StatoscopeWebpackPlugin()
],
...
}
Далее при запуске билда проекта у нас дополнительно откроется страница со статистикой проекта. Вот какая стата была получена после eject проекта.
Вес проекта в сжатом виде 371,35кб, скорость сборки 36,4 секунды, 1 чанк.
Удаляем лишнее
После эджекта я начал смотреть установленные плагины и пакеты.
Вот список того, что я почистил:
1) Пакет camelcase (использовался в jest файле)
2) Case sensitive paths webpack plugin, который следит за чувствительностью регистра в импортах
3) Пакет prompts
4) Identity obj proxy (в основном нужен для тестирования css модулей, так как надо определять имя объекта как класс, а у нас в проекте Styled-components)
5) Sass-loader
6) Tailwind
7) Semver (для сравнения версии реакт, нужен был только чтобы проверить выше ли 16 версии, в проекте я поднимал версию до 18 и проверка уже точно не понадобится)
8) Sevents, так как это для подписки на уведомления в MacOS
9) Удалил кучу плагинов react-dev-utils, но пакет оставил, так как некоторые вещи, включая отображение в консоли cmd надписей разных цветов не работали (информативные сообщения при хот релоаде, которые настроены в create react app в консоли)
В итоге статистика показала мне уменьшение сжатой сборки на 0,02кб, скорость сборки не изменилась.
Тогда я полез в файлы настройки webpack, запуска проекта, билда и т.п. и понял, что без бутылки разобраться полностью в этих файлах быстро не получится и скорее всего даже нет смысла.
И я принял волевое решение полностью снести эту сборку и поставить свою..
Начинаем сначала
На моем гитхабе где-то год назад я создал проект со сборкой вебпака, настройками линта и т.п. Сборка вебпака там достаточно стандартная, с настройкой для sass, css-modules и т.п., но без настройки под React Router и сильной оптимизации. Ее я взял за основу новой сборки. Если кому-то интересна эта сборка, то по ссылке можете посмотреть.
Сразу ставлю статоскоп, проверяю стату, не радуюсь)
Я начал наворачивать плагины для сжатия и оптимизации сборки.
Вот, что я поставил помимо того, что у меня уже стояло:
1) Terser Plugin. Этот плагин минифицирует и сжимает код, что позволяет хорошо уменьшить размер сборки. (Я даже сразу замерил стату и получил результат, который меня порадовал)
Стата с терсером
2) Css Minimizer Webpack Plugin для оптимизации CSS в сборке.
Первые два плагина ставятся в раздел minimizer вебпака
module.exports = {
...
optimization: {
minimize: true,
minimizer: [
new TerserPlugin(),
new CssMinimizerWebpackPlugin()
],
...
}
...
}
3) React Refresh Webpack Plugin. Он нужен больше для разработки, чтобы при hot-reload страница не перезагружалась, если меняется только визуальная составляющая. Помогает сохранять стейт приложения.
4) Hashed Module Ids Plugin. Он составляет хэши модулей в сборке на основе их относительных путей. Больше нужен также для удобства.
После этого я:
немного доработал сборку для работы с Styled-Components;
добавил в работу с svg лоадер
@svgr/webpack
, чтобы можно было нормально импортить svg;поставил нужные для проекта alias;
добавил
historyApiFallback: true
, чтобы работал React Router, иначе пути воспринимаются как гет запросы на сервер;прописал
client: { overlay: false }
, чтобы ошибки линтеров не лезли поверх экрана, так как это очень бесит;немного доработал tscoinfig.
В итоге я получил следующую статистику:
312,01 кб вес сжатой сборки, 28,1 секунд сборка и также 2 чанка.
Учитывая, что сам по себе проект небольшой, в принципе оптимизация выполнена успешно. Единственное, что я решил добавить - это разбиение всех пакетов в отдельные чанки. Для этого переработал блок optimization:
optimization: {
minimize: true,
minimizer: [
new TerserPlugin(),
new CssMinimizerWebpackPlugin()
],
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/) ?? 'package';
return `npm.${packageName[1].replace('@', '')}`;
}
}
}
},
},
После этого получаем следующее:
Вес увеличился до 334,46 кб, сборка стала чуть быстрее и все пакеты лежат в отдельных чанках.
Что в итоге?
В итоге могу сказать, что сборка CRA действительно имеет лишние плагины и сложный код для поддержки вебпака, но при этом вроде как и не так плоха.
Оптимизация получилась, хоть и не супер пупер результативная, но все-таки она есть, проект стал меньше весить в сжатом виде (хотел бы обратить внимание, что вес в не сжатом виде все-таки выше, чем в CRA) и теперь все разбивается на кучу чанков. Сейчас результаты не сильно большие, но думаю, что когда проект будет разрастаться, данная настройка поможет проекту.
Также прикладываю полный код webpack.config.js и tsconfig.json, которые используются для этой сборки.
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const StatoscopeWebpackPlugin = require('@statoscope/webpack-plugin').default;
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
const webpack = require('webpack')
const Dotenv = require('dotenv-webpack');
require('dotenv').config({path: path.resolve(__dirname, '.env')})
const mode = process.env.NODE_ENV || "development";
const port = process.env.PORT || 3000;
const devMode = mode === "development";
const target = devMode ? 'web' : 'browserslist';
const devtool = devMode && 'source-map';
module.exports = {
mode,
target,
devtool,
devServer: {
port,
open: true,
hot: true,
historyApiFallback: true,
client: {
overlay: false
},
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': '*',
},
onBeforeSetupMiddleware(devServer) {
devMode && require(path.resolve(__dirname, 'src/setupProxy.js'))(devServer.app);
},
},
entry: path.resolve(__dirname, 'src', 'index.tsx'),
output: {
path: path.resolve(__dirname, 'build'),
filename: 'js/[name].[contenthash].bundle.js',
chunkFilename: 'js/[id].[contenthash].js',
assetModuleFilename: 'assets/[hash][ext]',
publicPath: '/'
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin(),
new CssMinimizerWebpackPlugin()
],
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/) ?? 'package';
return `npm.${packageName[1].replace('@', '')}`;
}
}
}
},
},
resolve: {
extensions: ['.json', '.jsx', '.tsx', '.ts', '.js', '.mjs'],
alias: {
'~components': path.resolve(__dirname, 'src/components'),
'~hooks': path.resolve(__dirname, 'src/hooks'),
'~types': path.resolve(__dirname, 'src/types'),
'~pages': path.resolve(__dirname, 'src/pages'),
'~utils': path.resolve(__dirname, 'src/utils'),
'~constants': path.resolve(__dirname, 'src/constants'),
'~helpers': path.resolve(__dirname, 'src/utils/helpers'),
'~layouts': path.resolve(__dirname, 'src/layouts'),
'~api': path.resolve(__dirname, 'src/api'),
},
},
plugins: [
new Dotenv({ path: path.resolve(__dirname, '.env'), systemvars: true }),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public', 'index.html'),
}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].bundle.css',
}),
new CleanWebpackPlugin(),
new ESLintPlugin({
extensions: ['ts', 'tsx'],
exclude: ['/node_modules/', '/.idea/', '/.vscode/'],
}),
new ReactRefreshWebpackPlugin({
overlay: false,
}),
new webpack.ids.HashedModuleIdsPlugin(),
new StatoscopeWebpackPlugin()
],
module: {
rules: [
{
test: /\.(c|sa|sc)ss$/i,
use: [
devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
esModule: true,
importLoaders: 1,
modules: {
mode: 'icss'
},
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [require('postcss-preset-env')],
},
},
},
],
},
{
test: /\.woff2?$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[ext]',
},
},
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
},
{
test: /\.svg$/,
use: [
{
loader: require.resolve('@svgr/webpack'),
options: {
prettier: false,
svgo: false,
svgoConfig: {
plugins: [{ removeViewBox: false }],
},
titleProp: true,
ref: true,
},
},
{
loader: require.resolve('file-loader'),
options: {
name: 'static/media/[name].[hash].[ext]',
},
},
],
issuer: {
and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
},
},
{
test: /\.tsx?$/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true
}
},
exclude: /node_modules/,
},
{
test: /\.m?jsx?$/i,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
},
};
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"lib": [
"dom",
"dom.iterable",
"es2016"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"downlevelIteration": true,
"emitDeclarationOnly": false
},
"include": [
"src"
],
"exclude": ["node_modules", "build"],
"extends": "./tsconfig.paths.json" //Лежат пути для alias
}
Надеюсь, кому-то эти настройки помогут, так как они достаточно универсальны.
Буду рад обсуждению сборки и получить какие-то советы по поводу того, что можно убрать или добавить, чтобы сборка получилась лучше.
Всем добра)
Пы.Сы. Телега
У меня есть свой telegram-канал, в котором я выкладываю разные статейки, посты и провожу мини квизы по программированию. Присоединяйтесь)
Комментарии (5)
Coler95
18.04.2023 08:22Почему не стал пробовать Vite ?) Судя по графикам и тестам в интернете , намного быстрее. Webpack'a
idmx Автор
18.04.2023 08:22К сожалению, не я решаю пул технологий и мне сказали пока Vite не трогать, оставить на будущие эксперименты, когда будет больше времени) Так бы сам попробовать его хотел на каком-нибудь коммерческом проекте
fr_ant
По опыту проще удалить CRA и написать свой конфиг, дальше жить и масштабироваться с ним будет легче
idmx Автор
Да, так и сделал по итогу ж) Единственное, что вокруг куча разных сборок, да и сам их пишешь не каждый день, поэтому хотелось бы понимать, нормально ли вообще делаешь или фигню катаешь какую-то