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