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
傳入進行使用,以後咱們能夠在瀏覽器控制檯成功打印出配置好的環境變量:
到這裏,咱們已經實現了經過環境變量來控制是否進行打包分析,而且能夠在源代碼中使用配置好的全局變量。