初級分析:使用webpack內置的stats
利用webpack內置的stats對象
它可以幫我們分析基本的一些信息,比如構建總共的時間,構建資源的大小
package.json 中使用 stats
指定輸出的是一個json對象,生成一個json文件
"scripts": {
"build:stats": "webpack --config webpack.prod.js --json > stats.json"
}
node.js中使用
const webpack = require('webpack')
const config = require('./webpack.config.js')('production')
webpack(config, (err, stats) => {
if (err) {
return console.error(err)
}
if (stats.hasErrors()) {
return console.error(stats.toString('errors-only'))
}
console.log(stats)
})
這兩種方式顆粒度太粗,看不出問題所在。想要分析實際的問題,比如哪個組件比較大,哪個loader耗的時間比較長,是無法很好的分析出來的。
速度分析:使用speed-measure-webpack-plugin
更好的分析webpack構建的速度,怎么找出構建速度問題所在。
使用speed-measure-webpack-plugin
可以看到每個loader和插件執行耗時,重點的關注耗時較長的loader或插件,針對這些做優化。
const SpeedMeatureWebpackPlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeatureWebpackPlugin()
const webpackConfig = smp.wrap({
plugins: [
new MyPlugin(),
new MyOtherPlugin()
]
})
速度分析插件作用
分析整個打包總耗時
每個loader和插件的耗時情況
體積分析:使用webpack-bundle-analyzer
它可以把我們的項目打包出來的文件會進行一個分析,能很方便的看出體積的大小。面積越大體積越大,我們可以重點關注這些進行優化。
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
構建完成后會在8888端口展示體積大小。
可以分析哪些問題
依賴的第三方模塊文件大小。
業務的組件代碼圖片大小,針對大的js可以做js的按需加載等優化操作。
使用高版本的webpack和Node.js
在 webpack 里做速度的優化。
在軟件這一塊,性能往往不是最大的問題,軟件不斷的迭代過程中,可以不斷的提升性能,對於構建而言同樣是適用的,所以推薦采用高版本的 webpack 和 node.js。
使用webpack4:優化原因
-
V8帶來的優化,很多對原生方法的優化(for of 代替 forEach、Map 和Set 代替 Object、includes 代替 indexOf)
-
默認使用更快的 md4 的 hash 算法
-
webpacks AST 可以直接從 loader 傳遞給 AST,減少解析時間
-
使用字符串的方法替代正則表達式
采用更高版本的node.js
高版本的node.js對原生的js API或數據結構是有做一些優化的。
驗證高版本node.js比低版本node.js性能更快,針對相同的api、相同的代碼做比較。
// 設置10000個key,運行100次
const runCount = 100
const keyCount = 10000
let map = new Map()
let keys = new Array(keyCount)
for (let i = 0; i < keyCount; i++) keys[i] = {}
for (let key of keys) map.set(key, true)
let startTime = process.hrtime()
for (let i = 0; i < runCount; i++) {
for (let key of keys) {
let value = map.get(key)
if (value !== true) throw new Error()
}
}
let elapsed = process.hrtime(startTime)
let [seconds, nanoseconds] = elapsed
console.log(elapsed)
let milliseconds = Math.round(seconds * 1e3 + nanoseconds * 1e-6)
console.log(`${process.version} ${milliseconds} ms`)
includes和indexOf的性能差異
const ARR_SIZE = 1000000
const hugeArr = new Array(ARR_SIZE).fill(1)
// includes
const includesTest = () => {
const arrCopy = []
console.time('includes')
let i = 0
while (i < hugeArr.length) {
arrCopy.includes(i++)
}
console.timeEnd('includes')
}
// indexOf
const indexOfTest = () => {
const arrCopy = []
console.time('indexOf')
for (let item of hugeArr) {
arrCopy.indexOf(item)
}
console.timeEnd('indexOf')
}
includesTest()
indexOfTest()
多進程/多實例構建
多進程/多實例構建:資源並行解析可選方案
多進程/多實例:使用HappyPack解析資源
原理:每次 webapck 解析一個模塊,HappyPack 會將它及它的依賴分配給 worker 線程中。
每次 webpack 解析一個模塊,webpack 自身開啟一個進程去解析這個模塊。HappyPack 會將這個模塊進行划分,比如有多個模塊,在 webpack compiler run 方法之后,到達 HappyPack,它會做一些初始化,創建一個線程池,線程池會將構建任務里的模塊進行分配 ,比如將某個模塊以及它的依賴分配給 HappyPack 其中的一個線程,以此類推,那么一個 HappyPack 的線程池可能會包括多個線程,這些線程會各自的處理這些模塊以及它的依賴。處理完成之后,會有一個通信的過程,將處理好的資源傳輸給 HappyPack 的主進程,完成整個構建的過程。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
// 'babel-loader',
'happypack/loader'
]
}
]
},
plugins: [
new HappyPack({
id: 'jsx',
threads: 4,
loaders: ['babel-loader?cacheDirectory=true']
}),
new HappyPack({
id: 'styles',
threads: 2,
loaders: ['style-loader', 'css-loader', 'less-loader']
})
]
}
多進程/多實例:使用thread-loader解析資源
webpack4 原生提供 thread-loader 這個模塊,它可以很好的替換 HappyPack,來做多進程/多實例的工作。
原理:跟 HappyPack 是差不多的。每次 webpack 解析一個模塊,thread-loader 會將它及它的依賴分配給worker 線程中。
在其他的loader之前放上thread-loader,做一系列的解析,最后會通過thread-loader進行處理。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'thread-loader',
options: {
workers: 3
}
},
'babel-loader'
]
}
]
}
}
多進程/多實例並行壓縮代碼
方法一:使用 webpack-parallel-uglify-plugin 插件
const ParallelUglifyPluging = require('webpack-parallel-uglify-plugin')
module.exports = {
plugins: [
new ParallelUglifyPluging({
uglifyJS: {
output: {
beautify: false,
comments: false
},
compress: {
warning: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true
}
}
})
]
}
方法二:uglifyjs-webpack-plugin 開啟 parallel 參數
webpack3 推薦采用的插件,不支持 es6 代碼的壓縮。
const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
plugins: [
new UglifyjsWebpackPlugin({
uglifyOptions: {
warnings: false,
parse: {},
compress: {},
mangle: true,
output: null,
tiplevel: false,
nameCache: null,
ie8: false,
keep_fnames: false
},
parallel: true
})
]
}
方法三:terser-webpack-plugin 開啟 parallel 參數
webpack4 默認使用的,支持es6代碼的壓縮。
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true
})
]
}
}
進一步分包:預編譯資源模塊
分包:設置Externals
思路:將react, react-dom基礎包通過cdn引入,不打入bundle中。
方法:使用html-webpack-externals-plugin。
缺點:一個基礎庫需要指定一個cdn,實際的項目中有很多包,需要引入的script標簽太多 。
通過split-chunks-plugin插件分離基礎包,
缺點:它每次還是會對基礎包進行分析。
進一步分包:預編譯資源模塊
分包來說,更好的方式。
思路:將react、react-dom、redux、react-redux基礎包和業務基礎包打包成一個文件。
方法:使用 DLLPlugin 進行分包,DllReferencePlugin 對 manifest.json 引用。manifest.json 是對分離出來的包的描述。
使用 DLLPlugin 進行分包
創建一個單獨的構建配置文件,一般命名為 webpack.ddl.js,DLLPlugin 也會提高打包的速度。
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
library: [
'react',
'react-dom'
]
},
output: {
path: path.join(__dirname, 'build/library'),
filename: '[name].[chunkhash].dll.js',
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
name: '[name].[hash]',
path: path.join(__dirname, 'build/library/[name].json'),
})
]
}
使用 DLLReferencePlugin 引用 manifest.json
在 webpack.config.js 中引入
module.exports = {
plugins: [
new webpack.DLLReferencePlugin({
manifest: require('./build/library/manifest.json')
})
]
}
充分利用緩存提升二次構建速度
緩存目的:提升二次構建速度。
緩存思路:
-
babel-loader 開啟緩存
-
terser-webpack-plugin 開啟緩存
const TerserPlugin = require('terser-webpack-plugin') module.exports = { optimization: { minimize: true, minimizer: [ new TerserPlugin({ cache: true }) ] } }
-
使用 cache-loader 或者 hard-source-webpack-plugin
-
針對模塊的緩存的開啟
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin') module.exports = { plugins: [ new HardSourceWebpackPlugin() ] }
-
有緩存的話node_modules下面會有一個cache目錄
縮小構建目標
縮小構建目標
目的:盡可能的少構建模塊。
比如 babel-loader 不解析 node_modules。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: 'node_modules'
}
]
}
}
減少文件搜索范圍
優化 resolve.modules 配置(減少模塊搜索層級)
resolve.modules 是模塊解析的過程,webpack 解析時,模塊的查找過程和 nodejs 的模塊查找是比較類似的,會從當前的項目找,沒找到會去找 node_modules。會依次去子目錄找模塊是否存在。
優化 resolve.mainFields 配置
找入口文件的時候,會根據 package.json 里的 main 字段查找,因為發布到 npm 的組件的 package.json 會遵守一定的規范,都會有 main 這個字段,可以設置查找的時候直接讀取 main 這個字段,這樣也會減少一些不必要的分析過程。比如 package.json 里面沒有這個 main ,那它再去讀取根項目下的 index.js,沒有再去找 lib 下面的 index.js,這就是它默認的查找過程,我們把這個默認的查找過程鏈路做一個優化,只找 package.json 中 main 字段指定的入口文件。
優化 resolve.extensions 配置
模塊路徑的查找,比如 import 一個文件,沒有寫后綴,webpack 會先去找 .js,沒有會找 .json,默認情況下webpack 只支持 js 和 json 的讀取。extensions 數組里可以再設置其他的文件,如 .jsx .vue .ts 等。不過這個數組里面的內容越多的話,查找消耗的時間也會越多,因此我們可以縮小 extensions 查找的范圍,比如只設置查找 .js,其他文件需要寫的時候寫全文件后綴。避免 webpack 做不必要的查找。
合理使用 alias
別名,簡短的縮寫。比如模塊的路徑,我們找 react,它可能找了一圈,最后肯定是會找到 node_modules 里面去,它會經歷一系列的查找過程,我們可以把這一系列的過程直接給它寫好,告訴它比如你遇到了 react,就直接從指定的這個路徑去找。這個也大大的縮短了查找的時間。
module.exports = {
// 子模塊的查找策略
resolve: {
alias: {
'react': path.resolve(__dirname, './node_modules/react/umd/react.production.min.js'),
'react-dom': path.resolve(__dirname, './node_modules/react-dom/umd/react-dom.production.min.js')
},
modules: [path.resolve(__dirname, 'node_modules')],
extensions: ['.js'],
mainFields: ['main']
}
}
使用Tree Shaking擦除無用的JavaScript和CSS
無用的css如何刪除掉?
PurifyCSS: 遍歷代碼,識別已經用到的 CSS class。
uncss: HTML 需要通過 jsdom 加載,所有的樣式通過 PostCSS 解析,通過 document.querySelector 來識別在 html 文件里面不存在的選擇器。
在 webpack 中如何使用 PurifyCSS?
使用 purgecss-webpack-plugin,它不能獨立使用,需要提取 css 為一個文件后才能使用。在 webpack4里需要和 mini-css-extract-plugin 配合使用,在 webpack3 里需要和 extract-text-webpack-plugin 配合使用。
const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgecssWebpackPlugin = require('purgecss-webpack-plugin')
const PATHS = {
src: path.join(__dirname, 'src')
}
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css'
}),
new PurgecssWebpackPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true })
})
]
}
使用webpack進行圖片壓縮
圖片資源相對是較大的,我們可以通過在線工具手動進行圖片的批量壓縮。構建工具一部分的職責就是將平時我們手動完成的事做成自動化。
圖片壓縮
要求:基於 Node 庫的 imagemin 或者 tinypng API。
使用:配置 image-webpack-loader。
module.exports = {
module: {
rules: [
{
test: /\.(png|svg|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[hash:8].[ext]'
}
},
{
loader: 'image-webpack-loader',
options: {
// bypassOnDebug: true, // webpack@1.x
// disable: true, // webpack@2.x and newer
mozjpeg: {
progressive: true,
quality: 65
},
// optipng.enabled: false will disable optipng
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.90],
speed: 4
},
gifsicle: {
interlaced: false,
},
// the webp option will enable WEBP
webp: {
quality: 75
}
}
}
]
}
]
}
}
Imagemin 的優點分析
-
有很多定制選項
-
可以引入更多第三方優化插件,例如pngquant
-
可以處理多種圖片格式
Imagemin 的壓縮原理
-
pngquant: 是一款 PNG 壓縮器,通過將圖像轉換為具有 alpha 通道(通常比 24/32 位 PNG 文件小 60-80%)的更高效的 8 位 PNG 格式,可顯著減小文件大小。
-
pngcrush: 其主要目的是通過嘗試不同的壓縮級別和 PNG 過濾方法來降低 PNG IDAT 數據流的大小。
-
optipng: 其設計靈感來自於 pngcrush。optipng 可將圖像文件重新壓縮為更小尺寸,而不會丟失任何信息。
-
tinypng: 也是將 24 位 png 文件轉化為更小有索引的 8 位圖片,同時所有非必要的 metadata 也會被剝離掉。
使用動態Polyfill服務
Babel 默認只轉換新的 JavaScript 句法(syntax),而不轉換新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局對象,以及一些定義在全局對象上的方法(比如Object.assign )都不會轉碼。比如 ES6 在 Array 對象上新增了 Array.from 方法。Babel 就不會轉碼這個方法。如果想讓這個方法運行,必須使用 babel-polyfill。
構建體積優化:動態polyfill
babel-polyfill
打包后體積:88.49k,占比 29.6%
Promise 的瀏覽器支持情況
Polyfill 方案
Polyfill Service原理
識別 User Agent,下發不同的 Polyfill。
如何使用動態 Polyfill service
polyfill.io 官方提供的服務
https://polyfill.io/v3/polyfill.min.js
基於官方自建polyfill服務
//huayang.qq.com/polyfill/v3/polyfill.min.js?unknown=polyfill&features=Promise,Map,Set
體積優化策略總結:
-
Scope Hoisting
-
Tree Shaking
-
公共資源分離
-
圖片壓縮
-
動態 polyfill