本文主要讲下React配合Webpack的一些优化,原项目在这里,有空会持续更新,欢迎关注和start,另外还有个没法使用HtmlWebpackPlugin插入chunks的issues请求哪位大佬帮忙解决下,谢谢~css
include
字段指明要转换的目录,使用exclude
排除目录:module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
exclude: path.resolve(__dirname, 'node_modules'),
loader: 'babel-loader'
}
]
}
};
复制代码
尽可能减小resolve.modules
, resolve.extensions
, resolve.mainFiles
, resolve.descriptionFiles
的值的数量html
resolve.modules
:使用resolve.modules
指定模块目录的路径:node
module.exports = {
...
resolve: {
modules: [path.resolve(__dirname, 'node_modules')]
}
};
复制代码
resolve.alias
:resolve.alias
使Webpack直接使用库的压缩版本,再也不对库进行解析,还可使用别名方便引用文件:react
module.exports = {
...
resolve: {
alias: {
Components: path.resolve(__dirname, 'src/components/'),
Utils: path.resolve(__dirname, 'src/utils/'),
react: patch.resolve(__dirname, './node_modules/react/dist/react.min.js')
}
}
};
复制代码
例如这样就能够直接使用React的压缩版本,每次构建时没必要再次解析。还能够经过别名引用文件,而没必要再打长长的引用路径:webpack
import ReactComponent from 'Components/ReactComponent';
复制代码
但这样的缺点是会没法使用Tree-Shaking,因此通常对React这种总体性比较强的库使用比较好,而像lodash这样的工具库仍是使用Tree-Shaking去除多余代码。git
resolve.extensions
:设置要解析文件后缀,默认值为:github
module.exports = {
...
resolve: {
extensions: ['.wasm', '.mjs', '.js', '.json']
}
};
复制代码
能够设置为本身要解析的文件类型,加快寻找速度:web
module.exports = {
...
resolve: {
extensions: ['.js', '.json', 'jsx']
}
};
复制代码
使用externals
能够防止某些库被打包,而经过其余方式引用库(如CDN),这样作的好处是当更新代码时不会影响库代码的缓存,用户只需下载新的代码便可。固然咱们也可使用chunk来把不常更新的库打包在另外一个文件,咱们下面再讲。json
例如,从CDN引入React:浏览器
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js" defer></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" defer></script>
<script src="./dist/index.js" defer></script>
复制代码
module.exports = {
...
externals: {
react: 'React',
'react-dom': 'ReactDOM'
},
}
复制代码
使用devtool
是很耗性能的,若是不须要用到它的话就不要设置它,若是须要用到且质量要很好可设为source-map
,不过这是很是耗时的,若是能够接受质量比较差的话,可以使用cheap-source-map
,官方推荐使用的是性能比较好质量比较差的cheap-module-eval-source-map
。
Webpack 4以后把公共代码提取工具从CommonChunksPlugin换成更好的SplitChunksPlugin。下面这个例子不使用externals
,而是把React和ReactDOM提取到公共模块代码。
module.exports = {
...
// externals: {
// react: 'React',
// 'react-dom': 'ReactDOM'
// },
optimization: {
...
splitChunks: {
chunks: 'all',
name: true,
automaticNameDelimiter: '-', // 模块间的链接符,默认为"~"
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10 // 优先级,越小优先级越高
},
default: { // 默认设置,可被重写
minChunks: 2,
priority: -20,
reuseExistingChunk: true // 若是原本已经把代码提取出来,则重用存在的而不是从新产生
}
}
}
},
}
复制代码
mode
可取值有:
production
:构建模式,会自动启用一些构建相关的插件,如压缩代码。module.exports = {
+ mode: 'production',
- plugins: [
- new UglifyJsPlugin(/* ... */),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
- new webpack.optimize.ModuleConcatenationPlugin(),
- new webpack.NoEmitOnErrorsPlugin()
- ]
}
复制代码
development
:开发模式,会启动一些开发相关的优化插件。module.exports = {
+ mode: 'development'
- devtool: 'eval',
- plugins: [
- new webpack.NamedModulesPlugin(),
- new webpack.NamedChunksPlugin(),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
- ]
}
复制代码
node
这里使用的版本为babel 7。由于如今大多数浏览器都已经支持ES6的语法,因此若是全部代码都转为ES5的话可能会产生大量的多余代码,因此这里只转换部分代码,那要兼容低版本的浏览器怎么办呢,别急,下面会讲到一些解决办法,咱们先来看下babel配置:
{
"presets": [
[
"@babel/react",
{
"modules": false // 关闭babel的模块转换,才能使用Webpack的Tree-Shaking功能
}
]
],
"plugins": [
"@babel/plugin-proposal-class-properties", // class,这个要放在前面,不然可能会报错
"@babel/plugin-transform-classes", // class
"@babel/plugin-transform-arrow-functions", // 箭头函数
"@babel/plugin-transform-template-literals" // 字符串模板
]
}
复制代码
当一些库的package.json
的sideEffects
有设置时,就能够很好地支持Tree-Shaking,如lodash:
{
"name": "lodash",
"sideEffects": false
}
复制代码
使用happypack可开启多线程来加速处理loader:
var HappyPack = require('happypack');
module.exports = {
...
rules: [
{
test: /\.(js|jsx)$/,
include: path.resolve(__dirname, 'src'),
exclude: path.resolve(__dirname, 'node_modules'),
use: 'happypack/loader?id=babel'
},
],
plugins: [
new HappyPack({
id: 'babel',
loaders:['babel-loader?cacheDirectory']
}),
],
}
复制代码
上面说到转换代码到ES5的话会很耗时且可能有不少多余代码,由于如今大多数浏览器都已经支持ES6语法,如今咱们来看看如何兼容较低版本的浏览器。
module
、nomodule
:可使用<script type="module" src="index.js"></script>
来加载ES6+的代码,由于支持这个属性的浏览器一定会支持async/await
、Promise
、class
这些属性,而不支持的浏览器则会选择忽略它,不进行加载。
因此也还须要一份ES5的脚原本兼容低版本的浏览器,使用<script nomodule src="index.es5.js"></script>
来加载ES5代码,能够识别nomodule
的浏览器会忽略它,而不能识别它的低版本浏览器则会加载它。这样就能够作到兼容到低版本的浏览器而较新的浏览器使用代码量少不少的ES6+代码。
可是这个方法也有缺点:当使用splitChunks
把代码分为较多的模块时,须要产生大量两个版本的代码。
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
复制代码
它会经过分析请求头信息中的 UserAgent 实现自动加载浏览器所需的 polyfills。若是你使用较新的版本访问上面的链接会发现没有多少代码,而用IE则会产生不少。这样咱们就可使用ES6+的代码和动态polyfill来兼容低版本浏览器,可是动态polyfill不能支持class
和箭头函数等等这些特性,因此就须要按上面那样配置babel来把这些转换成ES5的。想知道更多动态polyfill能够点这里。
有一些工具在开发时是不须要用到的,若是用了可能会大大减慢生成代码的速度,如UglifyJsPlugin
,在开发时不须要将代码进行压缩,还有如下工具也避免在开发时用到:
UglifyJsPlugin
ExtractTextPlugin
[hash]/[chunkhash]
AggressiveSplittingPlugin
AggressiveMergingPlugin
ModuleConcatenationPlugin
module.exports = {
// ...
output: {
pathinfo: false
}
};
复制代码
module.exports = {
...
optimization: {
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
}
};
复制代码
由于React的HTML元素都是写在JS文件中,因此通常致使构建出的JS文件很是大,而在加载和执行JS的漫长过程当中,用户的浏览器一直显示的都是白屏状态,首屏渲染的时间变得很是的长,不使用服务端渲染的话能够按如下方法进行一些改善。
可经过使用HtmlWebpackPlugin插件来为html文件添加loading,而不至于白屏。
var loading = {
ejs: fs.readFileSync(path.resolve(__dirname, 'template/loading.ejs')),
css: fs.readFileSync(path.resolve(__dirname, 'template/loading.css')),
};
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'template/index.ejs'),
hash: true,
loading: loading, // 在React渲染完前添加loading
}),
new ScriptExtHtmlWebpackPlugin({ // 给script标签加上defer
defaultAttribute: 'defer'
}),
]
}
复制代码
具体的模板代码看这里
prerender-spa-plugin能够生成单页面应用的首屏到HTML,原理是经过puppeteer访问相应路径抓取相应的内容,这里由于我一直装不上puppeteer,因此就不深刻讲了。
module.exports = {
...
new PrerenderSpaPlugin(
// Absolute path to compiled SPA
path.resolve(__dirname, '../dist'),
// List of routes to prerender
['/']
)
}
复制代码
可使用它来动态import React的组件,能够把一些不是那么重要的组件先分离到chunks,而后再动态引入,能够提高渲染首屏的速度:
import Loading from './src/components/Loading';
import ReactDOM from 'react-dom';
import Loadable from 'react-loadable';
const LoadableApp = Loadable({
loader: () => import('./src/App'),
loading: Loading,
});
ReactDOM.render(LoadableApp, document.querySelector('#root'));
复制代码
暂时就写这么多优化的地方,之后有空会持续更新,有什么问题欢迎一块儿讨论~