版本:
"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的按需加載上,未完待續。。。