构建优化
loaders
尽量少使用不同的 loaders/plugins
使用 include 字段指明要转换的目录, 使用 exclude 排除目录:
- module.exports = {
- ...
- module: {
- rules: [
- {
- test: /\.js$/,
- include: path.resolve(__dirname, 'src'),
- exclude: path.resolve(__dirname, 'node_modules'),
- loader: 'babel-loader'
- }
- ]
- }
- };
复制代码
resolve
尽量减少 resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles 的值的数量
resolve.modules:
使用 resolve.modules 指定模块目录的路径:
- module.exports = {
- ...
- resolve: {
- modules: [path.resolve(__dirname, 'node_modules')]
- }
- };
复制代码
resolve.alias:
resolve.alias 使 webpack 直接使用库的压缩版本, 不再对库进行解析, 还可以使用别名方便引用文件:
- 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 的压缩版本, 每次构建时不必再次解析. 还可以通过别名引用文件, 而不必再打长长的引用路径:
import ReactComponent from 'Components/ReactComponent';
复制代码
但这样的缺点是会无法使用 Tree-Shaking, 所以一般对 React 这种整体性比较强的库使用比较好, 而像 lodash 这样的工具库还是使用 Tree-Shaking 去除多余代码.
resolve.extensions:
设置要解析文件后缀, 默认值为:
- module.exports = {
- ...
- resolve: {
- extensions: ['.wasm', '.mjs', '.js', '.json']
- }
- };
复制代码
可以设置为自己要解析的文件类型, 加快寻找速度:
- module.exports = {
- ...
- resolve: {
- extensions: ['.js', '.json', 'jsx']
- }
- };
复制代码
externals
使用 externals 可以防止某些库被打包, 而通过其他方式引用库 (如 CDN), 这样做的好处是当更新代码时不会影响库代码的缓存, 用户只需下载新的代码即可. 当然我们也可以使用 chunk 来把不常更新的库打包在另一个文件, 我们下面再讲.
例如, 从 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
使用 devtool 是很耗性能的, 如果不需要用到它的话就不要设置它, 如果需要用到且质量要很好可设为 source-map, 不过这是非常耗时的, 如果可以接受质量比较差的话, 可使用 cheap-source-map, 官方推荐使用的是性能比较好质量比较差的 cheap-module-eval-source-map.
splitChunks
Webpack 4 之后把公共代码提取工具从 CommonChunksPlugin https://webpack.js.org/plugins/commons-chunk-plugin/ 换成更好的 SplitChunksPlugin https://webpack.js.org/plugins/split-chunks-plugin/ . 下面这个例子不使用 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
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,Tree-Shaking
这里使用的版本为 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
使用 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']
- }),
- ],
- }
复制代码
其他
把代码构建到 ES6+
上面说到转换代码到 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 把代码分为较多的模块时, 需要产生大量两个版本的代码.
动态 polyfill
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
复制代码
它会通过分析请求头信息中的 UserAgent 实现自动加载浏览器所需的 polyfills. 如果你使用较新的版本访问上面的连接会发现没有多少代码, 而用 IE 则会产生很多. 这样我们就可以使用 ES6 + 的代码和动态 polyfill 来兼容低版本浏览器, 但是动态 polyfill 不能支持 class 和箭头函数等等这些特性, 所以就需要按上面那样配置 babel 来把这些转换成 ES5 的. 想知道更多动态 polyfill 可以点这里 https://c7sky.com/polyfill-io.html .
开发优化
避免使用构建时才使用到的工具
有一些工具在开发时是不需要用到的, 如果用了可能会大大减慢生成代码的速度, 如 UglifyJsPlugin, 在开发时不需要将代码进行压缩, 还有以下工具也避免在开发时用到:
- UglifyJsPlugin
- ExtractTextPlugin
- [hash]/[chunkhash]
- AggressiveSplittingPlugin
- AggressiveMergingPlugin
- ModuleConcatenationPlugin
不要输出路径信息
- module.exports = {
- // ...
- output: {
- pathinfo: false
- }
- };
复制代码
关闭部分构建优化
- module.exports = {
- ...
- optimization: {
- removeAvailableModules: false,
- removeEmptyChunks: false,
- splitChunks: false,
- }
- };
复制代码
React 优化
因为 React 的 HTML 元素都是写在 JS 文件中, 所以一般导致构建出的 JS 文件非常大, 而在加载和执行 JS 的漫长过程中, 用户的浏览器一直显示的都是白屏状态, 首屏渲染的时间变得非常的长, 不使用服务端渲染的话可以按以下方法进行一些改善.
添加首屏 loading
可通过使用 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'
- }),
- ]
- }
复制代码
具体的模板代码看这里 https://github.com/MrWindlike/webpack-demo/tree/master/template
prerender-spa-plugin
prerender-spa-plugin 可以生成单页面应用的首屏到 HTML, 原理是通过 puppeteer 访问相应路径抓取相应的内容, 这里因为我一直装不上 puppeteer, 所以就不深入讲了.
- module.exports = {
- ...
- new PrerenderSpaPlugin(
- // Absolute path to compiled SPA
- path.resolve(__dirname, '../dist'),
- // List of routes to prerender
- ['/']
- )
- }
复制代码
React Loadable
可以使用它来动态 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'));
复制代码
暂时就写这么多优化的地方, 以后有空会持续更新, 有什么问题欢迎一起讨论~
来源: https://juejin.im/post/5b83aa3ef265da43506e986e