Tree Shaking
Tree Shaking
的中文意思是:摇树,它能够移除JavaScript
上下文中的未引用代码(dead-code
)。它只支持es6
中的import
和export
语法,因此并不会应用到require
语法中。css
咱们先经过一个简单的例子来简单理解下Tree Shaking
到底有什么做用:html
// math.js
const add = () => {
console.log('add');
}
const minus = () => {
console.log('minus');
}
// console.log('math');
export { add, minus }
// main.js
import { add } from './utils/math'
add();
复制代码
这里咱们新建了一个math.js
文件,并导出了add
和minus
俩个方法,而在main.js
中,咱们只是用到了add
方法。这里没有用到的minus
就是上下文中未引用代码,须要咱们在打包时删除掉minus
方法。node
在开发环境中,咱们要在webpack
中进行以下配置:react
optimization: {
// 该选项在production环境中默认开启
usedExports: true
}
复制代码
这样以后webpack
能够识别出哪些代码是用到的,哪些代码是没有用到的,但是并不会将代码进行移除:
webpack
咱们还要结合package.json
的sideEffects
属性来实现:git
{
// sideEffects能够将文件标记为没有反作用
"sideEffects": false
}
复制代码
这里的sideEffects
用来指定有反作用的文件,它也能够配置为一个数组:es6
{
"sideEffects": [
"./src/some-side-effectful-file.js",
"*.css"
]
}
复制代码
反作用: 在导入时会执行特殊行文的代码,而不是仅仅暴露一个或多个
export
。上边代码中被注释掉的console.log('math')
就是反作用。github
对于生产环境,它的usedExports
属性默认为true
,即支持tree shaking
,而后自动识别经过import
和export
语法导入和导出模块的文件中没有引用到的部分而进行删除,咱们只须要指定mode:production
便可。web
要想使用tree shaking
,须要知足如下几点要求:npm
es2015
模块语法package.json
文件中添加sideEffect
配置项来制定反作用文件production
模式来开启optimization
的一些默认优化(好比usedExports:true
和代码压缩)通过实际测试,发如今设置package.json
中的sideEffects
只是在生产环境生效,并且当移除该配置项的时候,对应没有用到的代码也不会进行打包,因此这里先不设置sideEffects
。
Code Splitting
)随着咱们项目的功能和需求的不断扩展,所生成代码的体积也会愈来愈大,若是这些内容都加载到入口文件的话,会致使项目的加载时间愈来愈长。
webpack
中的代码分割能够防止一个文件打包后的体积过大而致使加载时间过长的问题。code splitting
能够把代码分割到不一样的bundle
中,而后能够按需加载或并行加载文件。这样咱们能够获取到更小的打包资源,并经过控制资源加载优先级,来合理设置页面加载时间。
webpack
默认支持对es6
中的import()
语法引入的模块进行代码分割,这里咱们以lodash
的引入为例,代码和打包结果以下:
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
class App extends Component {
state = {
number: 10,
text: ''
}
componentDidMount = () => {
this.dynamicLodash()
}
dynamicLodash = () => {
import('lodash').then(({ default: _ }) => {
this.setState({ text: _.join([1, 2, 3], '-') })
})
}
render() {
const { text } = this.state
return (
<div> hello Webapck React <h2>{this.state.number}</h2> <h1>{text}</h1> </div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root')) 复制代码
这里咱们经过import()
语法来动态引入loadash
,和使用import
引入的效果区别以下:
分割前:
为了能够更清晰的看到打包出来的文件的信息,咱们能够经过webpack
提供的魔法注释(magic comments
)来对分割的chunk
进行命名:
import(/*webpackChunkName: "lodash"*/'lodash')
复制代码
打包后效果以下:
SplitChunksPlugin
的配置学习有小伙伴可能注意到,咱们在项目中还引入了React
和ReactDOM
。像这样使用同步方式引入的代码能不能也进行代码分割呢?
答案是能够的,配置以下:
// webpack.config.js
optimization: {
splitChunks: {
// 代码分割的类型,能够设置为'all','async','initial',默认是'async`
// 'all': 对同步和异步引入模块都进行代码分割
// 'async: 只对异步引入模块进行代码分割
// 'initial': 只对同步代码进行代码分割
chunks: 'all',
// 代码分割模块的最小大小要求,不知足不会进行分割,单位byte
minSize: 30000,
// 若是分割模块大于该值,还会再继续分割,0表示不限制大小
maxSize: 0,
// 最小被引用次数,只有在模块上述条件而且至少被引用过一次才会进行分割
minChunks: 1,
// 最大的异步按需加载次数
maxAsyncRequests: 5,
// 最大的同步按需加载次数
maxInitialRequests: 3,
// 分割模块打包chunk文件名分割符:'~'
automaticNameDelimiter: '~',
automaticNameMaxLength: 30,
// 分割文件名,设置为true会自动生成
name: true,
cacheGroups: { // 缓存组
vendors: {
// 分割模块匹配条件
test: /[\\/]node_modules[\\/]/,
// 权重
priority: -10
},
default: {
minChunks: 2,
priority: -20,
// 是否使用已有的chunk,设置为true表示若是使用到的文件已经被分割过了
// 就不会再进行分割,生成新的分割文件
reuseExistingChunk: true
}
}
}
}
复制代码
这里咱们只将chunks
设置为all
,其它的使用splitChunksPlugin
的默认配置便可:
Prefetching和PreLoading
当进行了代码分割以后,有些分割后的模块可能并不须要进行当即加载,咱们能够先将一些必要的内容先进性加载,以后再在浏览器和网络的空闲时间,加载其它内容。
工做中的使用场景是这样的:咱们常常用到的模态框组件,并不须要在页面一开始就加载资源,而是须要在用户点击以后才显示。因此咱们能够将这部分资源在页面主要内容加载完成后,利用浏览器和网络的空闲时间来加载模态框对应的资源,能够很好的减小浏览器的压力,合理利用带宽资源来提升用户提验和页面加载性能。
在webpack
中为咱们提供了Prefetching
和Preloading
这俩个方法来进行资源加载优化:
prefetch
: 加载的内容可能会在将来的任什么时候间被使用,它会在主文件加载完毕而且利用浏览器的空闲时间来进行资源请求preloading
: 加载的内容会被主文件当即用到,拥有中等程度的资源加载优先权,而且会在页面加载时当即和主文件平行使用浏览器提供的资源。这里咱们分别经过prefetch
和preloading
来加载lodash
和dayjs
模块,看看它们之间的区别:
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
class App extends Component {
state = {
number: 10,
text: '',
time: ''
}
componentDidMount = () => {
}
dynamicLodash = () => {
import(
/* webpackChunkName: "lodash" */
/* webpackPrefetch: true */
'lodash').then(({ default: _ }) => {
this.setState({ text: _.join([1, 2, 3], '-') })
})
}
dynamicDayjs = () => {
import(
/* webpackChunkName: "dayjs" */
/* webpackPreload: true */
'dayjs').then(({ default: dayjs }) => {
this.setState({ time: dayjs(new Date()) })
})
}
render() {
const { text, time } = this.state
return (
<div> hello Webapck React <h2>{this.state.number}</h2> <h1>{text}</h1> <h1>{time}</h1> <button onClick={this.dynamicLodash}>load lodash</button> <button onClick={this.dynamicDayjs}>load dayJs</button> </div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root')) 复制代码
MiniCssExtractPlugin
拆分css
代码在上文中咱们介绍了JavaScript
代码的分割,这里咱们介绍如何将CSS
文件从JavaScript
中分离出来,并经过link
标签引入到html-webpack-plugin
生成的html
文件中。
这须要使用到webpack
的一个插件:MiniCssExtractPlugin
。首先咱们来安装它
yarn add mini-css-extract-plugin -D
复制代码
而后进行以下配置:
// webpack.config.js
// module.rules
// 使用MiniCssExtractPlugin.loader来替换以前的style-loader
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
]
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
// 开启css模块化
modules: true,
// 在css-loader前应用的loader的数量:确保在使用import语法前先通过sass-loader和postcss-loader的处理
importLoaders: 2
}
},
'postcss-loader',
'sass-loader'
]
},
plugins: [
// 自动引入打包后的文件到html中:
// 对于每次打包都会从新经过hash值来生成文件名的状况特别适用
// 也能够经过template来生成一个咱们本身定义的html模板,而后帮咱们把打包后生成的文件引入
new HtmlWebpackPlugin({
filename: 'index.html', // 生成html文件的文件名
template: absPath('../index.html') // 使用的html模板
}),
// 在插件中添加对应的配置,配置项和出口文件的配置内容相同
new MiniCssExtractPlugin({
// 也能够指定生成目录:'static/css/[name]_[hash:8].css'(生成到static/css目录下)
filename: '[name]_[hash:8].css',
chunkFilename: '[name]_[hash:8]_chunk.css',
}),
]
复制代码
能够看到已经成功将css
文件进行了拆分并在index.html
中引入:
bundle analysis
)在咱们把代码打包好将要部署到服务器以前,若是想要看一下各个模块打包后的体积大小的话,须要分析webpack
的打包输出结果。官方文档相关知识在这里:传送门。
这里咱们经过官方推荐的插件webpack-bundle-analyzer
来进行打包体积分析。
仍是熟悉的套路,咱们须要先经过yarn
来安装一下:
yarn add webpack-bundle-analyzer -D
复制代码
而后在plugins
中进行配置:
// webpack.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
复制代码
这样当咱们执行yarn build
的时候,就会自动打开浏览器窗口,并为咱们展现每一个打包文件的体积:
lodash
占据了比较大的体积,而咱们只是使用到了它的
join
方法,这就是咱们能够想办法进行优化的一个点。
在上一小节中,咱们使用了插件来进行打包文件分析,但是这样打包以后会包含一些映射文件,若是就这样将它们部署到服务器的话就会增大服务器的压力,进而可能会影响到页面的性能。
为了解决这个问题,咱们能够设置一个环境变量,经过使用不一样的环境变量来应用不一样的webpack
插件,从而减小没有必要的打包插件。
在webpack
中咱们能够在命令行中经过--env
来指定任意的环境变量,以后咱们就能够在webpack.config.js
中访问到配置好的环境变量,而后根据不一样变量来进行不一样的操做。当使用环境变量后,咱们的配置文件必需要写成一个函数:
// webpack.config.js
module.exports = env => {
// Use env.<YOUR VARIABLE> here:
return {
// set some configuration here
}
}
复制代码
首先咱们在package.json
中添加新的打包命令,专门用来进行打包后的代码分析:
以后咱们要根据配置好的环境变量来判断是否使用BundleAnalyzerPlugin
插件:
// webpack.prod.js
module.exports = (env) => {
return merge(baseConfig, {
mode: 'production',
devtool: 'cheap-module-source-map',
plugins: [
new CleanWebpackPlugin(),
env.MODE === 'analysis' && new BundleAnalyzerPlugin(),
].filter(Boolean)
})
}
复制代码
最后咱们会经过Array.filter
方法来将数组中返回值为false
的元素进行过滤。
有些时候,咱们还须要在项目中使用配置好的不一样环境变量,这里咱们须要webpack.DefinePlugin
插件来进行建立编译时能够配置的全局常量:
// webpack.config.js
module.exports = (env) => {
return {
plugins: [
// other plugins ...
new webpack.DefinePlugin({
// 写法规定:可使用 '"production"' 或者使用 JSON.string('production')
'process.env.MODE': JSON.stringify(`${env.MODE}`)
})
]
}
};
// webpack.dev/prod.js
module.exports = (env) => {
// 将环境变量传入
return merge(baseConfig(env), {
// dev/prod webpack configuration
})
}
复制代码
这里咱们将webpack.config.js
改成了函数的形式,并将环境变量env
传入进行使用,以后咱们能够在浏览器控制台成功打印出配置好的环境变量:
到这里,咱们已经实现了经过环境变量来控制是否进行打包分析,而且能够在源代码中使用配置好的全局变量。