记一次webpack优化 create-react-app 从单个文件变为多个文件打包将公共依赖,公共代码整理到特定js文件里面
版本:
"webpack": "3.5.1",
"react": "^15.6.2",
"antd": "^3.26.7",
1.项目本来是一直跑在内网的,原先项目build之后是在一个js包里面,导致包基本上都在3M+ 甚至到5M+,本来在内网上,倒也速度到也不太慢,不过最近有考虑可能要放到外网上展示,所以要搞一下js大小,不然加载慢的一批
2.核心要求:js文件要尽量小,加载速度要快 优先于 打包速度 ,最好能打包速度快的同时 能js文件能尽量小
3.开始
1).不作任何改变的话,文件大小5M,时间110s
2).先考虑将单个文件拆分:从router入手,将文件变为懒加载或者说按需加载
//router.js import {asyncComponent} from '@utils/loadable'; const Login = asyncComponent(()=> import(/* webpackChunkName:'Login' */"../pages/Login/index"))//中间的注释可以让webpack按照这个名字打包 便于分辨
//loadable.js import React from 'react' import Loadable from 'react-loadable' import { Button, Result, Spin } from 'antd' import store from '@store/store' const extra = ({ retry }) => ( <Button type='primary' onClick={retry}>重新加载</Button> ) const ErrorComponent = props => ( <Result status='500' title='504' subTitle='似乎出了点问题' extra={extra(props)} /> ) const LoadingComponent = ( <div style={{ textAlign: 'center', marginTop: 100 }}> <Spin size='large'/> </div> ) const Loading = props => { if (props.error || props.timedOut) { return <ErrorComponent {...props}/> } else { return LoadingComponent } } const DefaultProps = { loading: Loading, timeout: 20000, // 20 seconds delay: 300 // 0.3 seconds } /** * 异步加载页面 * @param component * @param callback * @returns {*} */ export function asyncComponent (component, callback) { return Loadable({ ...DefaultProps, loader: component, render (loaded, props) { callback && callback() const Component = loaded.default props = {...props, ...store.getState().Task} return <Component {...props}/> } }) } /** * 异步加载页面 同时 添加其他文件 * @param component * @param models * @returns {*} */ export function asyncComponentWithModels (component, models) { const name = 'componentName' return Loadable.Map({ ...DefaultProps, loader: { [name]: component, ...models }, render (loaded, props) { const Component = loaded[name].default return <Component {...props}/> } }) }
这样,开始直接打包,然后发现最新打包时间需要240s左右,而且每一个包都很大少则几百K,多则1,2M,,,继续,然后发现里面很多重复的包比如echarts,moment等等(可以用webpack-bundle-analyzer和speed-measure-webpack-plugin分析你的包,百度很简单)
按照这个思路,有两个方法dll和CommonsChunkPlugin两种
⑴dll
之所以需要这个js文件是为了解决antd的按需加载,不过结果不尽人意
//package.json新增命令 "build:dll": "cross-env NODE_ENV=production node scripts/dll.js",//如果提示cross-env不是一个命令的话 npm i cross-env 就可以了
//根目录script新增dll.js const webpack = require('webpack') const chalk = require('chalk'); const { generator } = require('../config/webpack.dll.config') // const compilerAntd = webpack(reactConfig); new Promise((resolve, reject) => { const reactConfig = generator('react',[ 'react', 'react-dom', 'react-redux', 'redux', 'react-router', 'react-loadable', 'echarts', 'moment', 'jquery', 'html2canvas', 'jspdf' ], false) const compilerReact = webpack(reactConfig); compilerReact.run((err, stats) => { if(err){ console.log(err) } resolve(stats) }) }).then((stats) => { const antdConfig = generator('antdVendor', [ 'antd/lib/button', 'antd/lib/modal', 'antd/lib/page-header', 'antd/lib/table', 'antd/lib/card', 'antd/lib/form', 'antd/lib/input', 'antd/lib/icon', 'antd/lib/message', 'antd/lib/select', 'antd/lib/descriptions', 'antd/lib/input-number', 'antd/lib/tag', 'antd/lib/date-picker', 'antd/lib/inputNumber', 'antd/lib/dropdown', 'antd/lib/progress', 'antd/lib/spin', 'antd/lib/checkbox', 'antd/lib/col', 'antd/lib/row', 'antd/lib/config-provider', 'antd/lib/layout', 'antd/lib/menu', 'antd/lib/radio', 'antd/lib/switch', 'antd/lib/tree', 'antd/lib/upload', 'antd/lib/typography', ], true) const compilerAntd = webpack(antdConfig); compilerAntd.run((err, stats) => { if(err){ console.log(err) }else{ console.log(chalk.green(`success!!!`)); } }) })
//config文件新增webpack.dll.config.js // webpack.dll.config.js const Webpack = require('webpack') const fs = require('fs-extra'); const path = require('path'); const paths = require('./paths'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const WebpackBar = require('webpackbar'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; function generator(name, arr, needDepend){ !needDepend && fs.emptyDirSync(paths.vendorSrc); return { devtool: false, entry: { [name]: arr, }, output: { filename: "[name].dll.[hash].js", path: paths.vendorSrc, libraryTarget: 'var', library: '_dll_[name]_[hash]' }, plugins: [ new WebpackBar(), new Webpack.DefinePlugin({ "process.env": { NODE_ENV: JSON.stringify("production") } }), new Webpack.DllPlugin({ path: paths.vendorSrc + '/[name].manifest.json', name: '_dll_[name]_[hash]' }), new HtmlWebpackPlugin({ filename: paths.appHtml, template: needDepend ? paths.appHtml : paths.appHtmlTemplate, inject: true, minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, } }), new Webpack.optimize.UglifyJsPlugin({ compress: { warnings: false, } }), needDepend && new Webpack.DllReferencePlugin({ manifest: require(path.join(__dirname, '../public/vendor/react.manifest.json')) }), new BundleAnalyzerPlugin({ // 可以是`server`,`static`或`disabled`。 // 在`server`模式下,分析器将启动HTTP服务器来显示软件包报告。 // 在“静态”模式下,会生成带有报告的单个HTML文件。 // 在`disabled`模式下,你可以使用这个插件来将`generateStatsFile`设置为`true`来生成Webpack Stats JSON文件。 analyzerMode: 'server', // 将在“服务器”模式下使用的主机启动HTTP服务器。 analyzerHost: '127.0.0.1', // 将在“服务器”模式下使用的端口启动HTTP服务器。 analyzerPort: name == "react" ? 8887 : 8886, // 路径捆绑,将在`static`模式下生成的报告文件。 // 相对于捆绑输出目录。 reportFilename: 'report.html', // 模块大小默认显示在报告中。 // 应该是`stat`,`parsed`或者`gzip`中的一个。 // 有关更多信息,请参见“定义”一节。 defaultSizes: 'parsed', // 在默认浏览器中自动打开报告 openAnalyzer: true, // 如果为true,则Webpack Stats JSON文件将在bundle输出目录中生成 generateStatsFile: false, // 如果`generateStatsFile`为`true`,将会生成Webpack Stats JSON文件的名字。 // 相对于捆绑输出目录。 statsFilename: 'stats.json', // stats.toJson()方法的选项。 // 例如,您可以使用`source:false`选项排除统计文件中模块的来源。 // 在这里查看更多选项:https: //github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21 statsOptions: null, logLevel: 'info' //日志级别。可以是'信息','警告','错误'或'沉默'。 }) ].filter(Boolean), } } module.exports = { generator }
//修改paths.js 在最后面新增 vendorSrc: resolveApp('public/vendor'), appHtmlTemplate: resolveApp('public/indexTemplate.html'),
//在webpack.config.prod.js 修改或者webpack.config.js修改 module.exports = { plugins: [ new webpack.DllReferencePlugin({ manifest: require(path.join(__dirname, '../public/vendor/react.manifest.json')) }) ] }
这样运行npm run build:dll 应该会成功打出两个js和两个json并且都挂在html里面了(省的自己手动写js文件路径了)
⑵CommonsChunkPlugin
//在webpack.config.prod.js 修改或者webpack.config.js修改 module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'vender', minChunks: function (module, count) { return ( module.resource && /\.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, '../node_modules') ) === 0 ) } }), new webpack.optimize.CommonsChunkPlugin({ name: 'components', minChunks: function (module, count) { return ( module.resource && /\.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, '../src') ) === 0 ) } }), new webpack.optimize.CommonsChunkPlugin({ name: "manifest", chunks: ['components', "vender"] }) }
经过这一些系列的改动(antd的按需加载还是没有完全搞定,还是会有重复打包的地方),现在基本是除了dll包是在1M+,别的都是在500kb一下了,也可以讲究一下了
还有一个优化的地方就是使用多线程
const HappyPack = require('happypack');
{
test: /\.(js|jsx)$/,
include: paths.appSrc,
use: ['happypack/loader?id=babel'],//在这里修改使用happypack id是跟后面的值对照
}
//plugin
//开启多线程
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
// 注意:loaders 是 use 的别名
use: [ 'cache-loader', {
loader: 'babel-loader',
options: {
plugins: [
['import', [{ libraryName: "antd", style: 'less' }]],
],
cacheDirectory: true,
}
}],
threads: 10
// ... 其它配置项
})
也能减少一点构建时间
最后构建时间压缩到80s左右,开启gzip情况下单个js文件都在200KB一下,现在的问题基本上都是在antd的按需加载上,未完待续。。。