背景

按照 Ant Design 官网用 React 脚手构建的后台项目,刚接手项目的时候大概30条路由左右,我的用的机子是 Mac 8G 内存,打包完成需要耗时2分钟左右,决定优化一下。

项目技术栈: React + React Router + TypeScript + Ant Design

构建时间慢可能的原因:

  1. React 脚手架默认打包构建出来的文件包含 map 文件

  2. Ant Desigin 以及项目中使用的第三方模块太大

  3. babel-loader 编译过程慢

React 脚手架修改 Webpack 配置方案:

  1. npm run rject 暴露出 Webpack 配置信息,直接进行修改。

  2. 使用 react-app-rewired (一个对 create-react-app 进行自定义配置的社区解决方案)Ant Design 官网推荐。

自定义Webpack配置步骤:

  1. 基础配置

  2. 开发环境配置

  3. 生产环境配置

使用 customize-cra 修改React 脚手架配置实践

1、准备工作

npm i react-app-rewired customize-cra --save-dev 安装 react-app-rewired ,customize-cra,它提供一些修改 React 脚手架默认配置函数,具体参见:https://github.com/arackaf/customize-cra

安装后,修改 package.json 文件的 scripts

"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
}

项目根目录创建一个 config-overrides.js 用于修改Webpack 配置。

Ant Desigin 提供了一个按需加载的 babel 插件 babel-plugin-import

antd-dayjs-webpack-pluginAnt Desigin 官方推荐的插件,用于替换moment.js

安装 npm i babel-plugin-import --save-dev,并修改config-overrides.js 配置文件

override函数用来覆盖React脚手架Webpack配置;fixBabelImports修改babel配置

const { override, fixBabelImports,addWebpackPlugin } = require('customize-cra');
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
}),
addWebpackPlugin(new AntdDayjsWebpackPlugin())
);

以上是Ant Desigin推荐的做法。

2、首屏加载优化

npm i react-loadable customize-cra --save安装react-loadable模块,然后在路由文件里使用如下,loading组件可以自定义。这样打包的时候会为每个路由生成一个chunk,以此来实现组件的动态加载。

需要安装"@babel/plugin-syntax-dynamic-import这个插件,编译import()这种语法

import Loadable from 'react-loadable';
const Index = Loadable({
loader:() => import('../components/Index'),
loading:SpinLoading
});

3、去掉 map 文件

首先安装依赖包webpack-stats-plugin webpack-bundle-analyzer 前者为了统计打包时间会在打包后的文件夹里生成一个stats.json文件,后者用来分析打包后的各个模块的大小。

process.env.GENERATE_SOURCEMAP = "false";用来去掉打包后的map文件

const { override, fixBabelImports } = require('customize-cra');
const { StatsWriterPlugin } = require("webpack-stats-plugin");
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
let startTime = Date.now()
if(process.env.NODE_ENV === 'production') process.env.GENERATE_SOURCEMAP = "false" // 自定义生产环境配置
const productionConfig = (config) =>{
if(config.mode === 'production'){
config.plugins.push(...[
new StatsWriterPlugin({
fields: null,
transform: (data) => {
let endTime = Date.now()
data = {
Time: (endTime - startTime)/1000 + 's'
}
return JSON.stringify(data, null, 2);
}
}),
new BundleAnalyzerPlugin()
])
}
return config
}
module.exports = override(
productionConfig,
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css',
}),
addWebpackPlugin(new AntdDayjsWebpackPlugin())
);

去掉map文件的打包时间,大约60s左右。查看打包后生成的分析图发现Ant Desigin的一些组件被重复打包,打包出来一共有13M多。

4、更细化分包

productionConfig配置添加,在入口文件添加vendors用来分离稳定不变的模块;common用来抽离复用模块;stylescss文件抽离成一个文件;

// 针对生产环境修改配置
const productionConfig = (config) =>{
if(config.mode === 'production'){
const splitChunksConfig = config.optimization.splitChunks;
if (config.entry && config.entry instanceof Array) {
config.entry = {
main: config.entry,
vendors: ["react", "react-dom", "react-router-dom", "react-router"]
}
} else if (config.entry && typeof config.entry === 'object') {
config.entry.vendors = ["react", "react-loadable","react-dom", "react-router-dom","react-router"];
}
Object.assign(splitChunksConfig, {
cacheGroups: {
vendors: {
test: "vendors",
name: 'vendors',
priority:10,
},
common: {
name: 'common',
minChunks: 2,
minSize: 30000,
chunks: 'all'
},
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
priority: 9,
enforce: true
}
}
})
config.plugins.push(...[
new StatsWriterPlugin({
fields: null,
transform: (data) => {
let endTime = Date.now()
data = {
Time: (endTime - startTime)/1000 + 's'
}
return JSON.stringify(data, null, 2);
}
}),
new BundleAnalyzerPlugin()
])
}
return config
}

以上实际打包运行大约35S左右,实际打包后的模块一共2.41M,打包后生成的分析图发现Ant Design有个图标库特别大,大约有520kb,但是实际项目中用到的图标特别少。到此不想继续折腾React脚手架了,还不如重新配置一套Webpack替换脚手架。

5、总结

  • React脚手架配置过重,对于庞大的后台系统不实用

  • Ant Design的图标库没有按需加载的功能

  • 修改React脚手架配置太麻烦

自定义Webpack配置实践

1、结果:替换掉脚手架后,陆陆续续新增路由到100条左右,打包耗时大概20s-30S之间,业务代码打包后1.49M,可以接受。

2、优化点:

  • 利用autodll-webpack-plugin插件,生产环境通过预编译的手段将Ant React 等稳定的模块全部先抽离出来,只打包编译业务代码。

  • babel-loader 开启缓存

  • 利用happypack加快编译速度

  • 生产环境不开启devtool

  • 细化分包

  • 针对项目轻量级配置(后台项目,基本只在 Chrome 浏览器下使用)

问题:

  • 抽离出来的第三方模块大概有3M多,经过zip大概也有800多Kb,首屏加载比较慢。如果结合externals属性将这些静态资源放置到CDN上或许加载会更快。

3、基础配置:

放于webpack.base.config.js文件

1、安装babel模块,使用的是babel7.0版本。

npm i install babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-transform-runtime --save-dev
npm i @babel/runtime-corejs3 --save

在更目录下创建babel.config.js babel的配置文件

module.exports = function (api) {
api.cache(true);
const presets = ["@babel/env","@babel/preset-react","@babel/preset-typescript"];
const plugins = [
["@babel/plugin-transform-runtime", {
corejs: 3,
}],
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-proposal-class-properties",
];
if (process.env.NODE_ENVN !== "production") {
plugins.push(["import", {
"libraryName": "antd", // 引入库名称
"libraryDirectory": "es", // 来源,default: lib
"style": "css" // 全部,or 按需'css'
}]);
}
return {
presets,
plugins
};
}

2、babel-loader配置:

const os = require('os');
const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
module.exports = {
.....
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
loaders: ['happypack/loader?id=babel']
}
},
plugins: [
new HappyPack({
id: 'babel',
threadPool: happyThreadPool,
loaders: [{
loader:'babel-loader',
options: {
cacheDirectory: true
}
}]
})
]
}

3、mini-css-extract-plugin 插件打包抽离CSS到单独的文件

var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var NODE_ENV = process.env.NODE_ENV
var devMode = NODE_ENV !== 'production';
var utils = require('./utils')
module.exports = {
....
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
options: {
// only enable hot in development
hmr: devMode,
// if hmr does not work, this is a forceful method.
reloadAll: true,
},
},
"css-loader"
],
},
]
},
plugins: [
new MiniCssExtractPlugin({
//utils.assetsPath 打包后存放的地址
filename: devMode ? '[name].css' : utils.assetsPath('css/[name].[chunkhash].css'),
chunkFilename: devMode ? '[name].css' : utils.assetsPath('css/[name].[chunkhash].css'),
ignoreOrder: false, // Enable to remove warnings about conflicting order
})
]
}

4、html-webpack-plugin 生成html 文件

const HtmlWebpackPlugin = require('html-webpack-plugin')
var htmlTplPath = path.join(__dirname, '../public/')
module.exports = {
....
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: htmlTplPath + 'index.html',
inject: true,
})
]
}

5、webpack.DefinePlugin生成业务代码可以获取的变量,可以区分环境

const webpack = require('webpack')
module.exports = {
....
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(devMode ? 'development' : 'production'),
'perfixerURL': JSON.stringify('//yzadmin.111.com.cn')
}),
}

4、开发环境配置:

放于webpack.development.config.js文件

var path = require('path');
var webpack = require('webpack');
var merge = require('webpack-merge');
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
var entryScriptPath = path.join(__dirname, '../src/')
var base = require('./webpack.base.config');
module.exports = merge(base, {
entry: {
app: [entryScriptPath+'index'] // Your appʼs entry point
},
output: {
path: path.join(__dirname, '../dist/'),
filename: '[name].js',
chunkFilename: '[name].[chunkhash].js'
},
module: {
},
plugins: [
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/),
new webpack.HotModuleReplacementPlugin(),
new FriendlyErrorsPlugin(),
]
});

start.js文件,用于npm start启动本地服务

var webpack = require('webpack');
var opn = require('opn')
var WebpackDevServer = require('webpack-dev-server');
var config = require('./webpack.development.config');
config.entry.app.unshift("webpack-dev-server/client?http://127.0.0.1:9000/", "webpack/hot/dev-server");
new WebpackDevServer(webpack(config), {
publicPath: config.output.publicPath,
hot: true,
historyApiFallback: {
index: '/public'
}
}).listen(9000, '127.0.0.1', function (err, result) {
if (err) {
return console.log(err);
}
opn('http://127.0.0.1:9000/')
});

5、生产环境配置:

放于webpack.production.config.js文件

const path = require('path')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.config');
const config = require('./webpack.env.config')
const utils = require('./utils')
const AutoDllPlugin = require('autodll-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
const webpackConfig = merge(baseWebpackConfig, {
module: {
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
entry: {
app: resolve('src/index'),
},
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
},
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
splitChunks: {
cacheGroups: {
common: {
test: /[\\/]src[\\/]/,
name: 'common',
chunks: 'all',
priority: 2,
minChunks: 2,
},
// 分离css到一个css文件
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
priority: 9,
enforce: true,
}
}
},
runtimeChunk: {
name:"manifest"
}
},
plugins: [
new AutoDllPlugin({
inject: true, // will inject the DLL bundles to index.html
filename: '[name].dll.js',
path: './dll',
entry: {
// 第三方库
react: ["react","react-dom","react-router", "react-router-dom",'react-loadable'],
antd: ['antd/es'],
untils: ['qs','qrcode'],
plugins:['braft-editor','js-export-excel']
}
})
]
}) if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin') webpackConfig.plugins.push(
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
} if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig

build.js 用于npm run build构建

process.env.NODE_ENV = 'production'

var ora = require('ora')
var path = require('path')
var chalk = require('chalk')
var shell = require('shelljs')
var webpack = require('webpack')
var config = require('./webpack.env.config')
var webpackConfig = require('./webpack.production.config') var spinner = ora('building for production...')
spinner.start()
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
shell.config.silent = true
shell.rm('-rf', assetsPath)
shell.mkdir('-p', assetsPath)
shell.cp('-R', 'static/*', assetsPath)
shell.config.silent = false webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n') console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})

修改package.json

"scripts": {
"start": "node webpack/start.js",
"build": "node webpack/build.js"
},

6、总结

  • 通过本次实践大致对Webpack有了初步了解,但关于Webpack的内部原理没有仔细探究过

  • 对一些脚手架做配置修改的前提是需要了解Webpack基础内容

  • 优化一个项目首先需要知道是大致什么原因造成的,可以利用的工具有speed-measure-webpack-plugin,webpack-bundle-analyzer

  • 项目虽然加入了typeScript但并没有很好的利用typeScript

基于 Ant Desigin 的后台管理项目打包优化实践的更多相关文章

  1. vue,vuex的后台管理项目架子structure-admin,后端服务nodejs

    之前写过一篇vue初始化项目,构建vuex的后台管理项目架子,这个structure-admin-web所拥有的功能 接下来,针对structure-admin-web的不足,进行了补充,开发了具有登 ...

  2. vue初始化项目,构建vuex的后台管理项目架子

    构架vuex的后台管理项目源码:https://github.com/saucxs/structure-admin-web 一.node安装 可以参考这篇文章http://www.mwcxs.top/ ...

  3. 【Vuejs】335-(超全) Vue 项目性能优化实践指南

    点击上方"前端自习课"关注,学习起来~ 前言 Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 D ...

  4. 基于 Blazui 的 Blazor 后台管理模板 BlazAdmin 正式尝鲜

    简介 BlazAdmin 是一个基于Blazui的后台管理模板,无JS,无TS,非 Silverlight,非 WebForm,一个标签即可使用. 我将在下一篇文章讨论 Blazor 服务器端渲染与客 ...

  5. docloud后台管理项目(开篇)

    最近朋友做app需要web做后台管理,所以花了一周时间做了这个项目. 废话不多说,开发环境是nginx+php5.3,使用thinkphp框架.是一个医疗器械数据统计的后台,业务功能很简单就是查看用户 ...

  6. vue+webpack+element-ui项目打包优化速度与app.js、vendor.js打包后文件过大

    从开通博客到现在也没写什么东西,最近几天一直在研究vue+webpack+element-ui项目打包速度优化,想把这几天的成果记录下来,可能对前端牛人来说我这技术比较菜,但还是希望给有需要的朋友提供 ...

  7. 基于fastadmin快速搭建后台管理

    FastAdmin是一款基于ThinkPHP5+Bootstrap的极速后台开发框架:开发文档 下面对环境搭建简要概述,希望后来者能少走弯路: 1. 百度搜索最新版wampserver, 安装并启动 ...

  8. 【vue】MongoDB+Nodejs+express+Vue后台管理项目Demo

    ¶项目分析 一个完整的网站服务架构,包括:   1.web frame ---这里应用express框架   2.web server ---这里应用nodejs   3.Database ---这里 ...

  9. 项目:Vue+node+后台管理项目小结

    序:本文主要分两块说:项目机制,具体用到的知识块. 1. 项目机制 项目的原型以vue-cli为原型,进行项目的初步构建.项目以node.js服务和webpack打包机制为依托,将.vue文件打包为浏 ...

随机推荐

  1. Python基础+Pythonweb+Python扩展+Python选修四大专题 超强麦子学院Python35G视频教程

    [保持在百度网盘中的, 可以在观看,嘿嘿 内容有点多,要想下载, 回复后就可以查看下载地址,资源收集不易,请好好珍惜] 下载地址:http://www.fu83.cc/ 感觉文章好,可以小手一抖 -- ...

  2. Servlet中文乱码解决方法

    程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件. 字节流和字符流的区别: 在Java.io包中操作文件内容的主要有两大类:字节流.字符流,两类都分为输入和输出操作. 在字节流中输 ...

  3. [原创].NET 分布式架构开发实战之二 草稿设计

    原文:[原创].NET 分布式架构开发实战之二 草稿设计 .NET 分布式架构开发实战之二 草稿设计 前言:本篇之所以称为草稿设计,是因为设计的都是在纸上完成的.反映了一个思考的过程. 本篇的议题如下 ...

  4. APP测试点归纳

    1.2测试周期 测试周期可按项目的开发周期来确定测试时间,一般测试时间为两三周(即 15个工作日), 根据项目情况以及版本质量可适当缩短或延长测试时间.正式测试前先向主管确认项目排期. 1.3测试资源 ...

  5. ***CodeIgniter框架集成支付宝即时到账支付SDK

    本文为CI集成支付宝即时到账支付接口 1.下载支付宝官方demo ;即时到账交易接口(create_direct_pay_by_user)(DEMO下载) 原文地址:https://doc.open. ...

  6. cookie的基本操作

    设置,读取,删除 var odate=new Date(); odate.setDate(odate.getDate()+14); document.cookie='user=blue;expires ...

  7. MATLAB——单层感知器

    1.创建一个感知器 实例 % example4_1.m p=[-,;-,] % 输入向量有两个分量,两个分量取值范围均为-~ % p = % % - % - t=; % 共有1个输出节点 net=ne ...

  8. spring事务——try{...}catch{...}中事务不回滚的几种处理方式

    当希望在某个方法中添加事务时,我们常常在方法头上添加@Transactional注解 @ResponseBody @RequestMapping(value = "/payment" ...

  9. mui 图片预览

    1. 图片放大滑动预览需要    mui.zoom.js   mui.previewimage.js 2. 调用  mui.previewImage(); 3. 滑动过程中禁止图片缩放  注释掉下面代 ...

  10. 【LeetCode】100. Same Tree (2 solutions)

    Same Tree Given two binary trees, write a function to check if they are equal or not. Two binary tre ...