Оптимизация настройки Webpack проекта на CRA

от автора

Всем привет! Один из проектов на работе у нас изначально создан на 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-канал, в котором я выкладываю разные статейки, посты и провожу мини квизы по программированию. Присоединяйтесь)

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Как вам приведенная сборка?

33.33% Хорошая, можно брать5
33.33% Норм, но можно лучше5
33.33% Так себе5

Проголосовали 15 пользователей. Воздержались 3 пользователя.

ссылка на оригинал статьи https://habr.com/ru/articles/729714/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *