# webpack构建速度及体积优化

# 初级分析:stats构建统计信息分析

通过webpack添加stats可设置在构建过程中打印统计信息。
亦可通过package.json中使用stats,生成stats.json日志。

{
    "scripts":{
        "build:stats": "webpack --config webpack.prod.js --json > stats.json"
    }
}

亦或通过新建stats.js运行脚本打印统计信息。

const webpack = require('webpack')
const config = require('./webpack.prod')

webpack(config,(err,stats) => {
    if(err){
        return console.error(err)
    }
    if(stats.hasErrors()){
        return console.error(stats.toString('errors-only'))
    }
    console.log(stats)
})

通过stats进行统计信息分析得出的数据相对比较粗糙,比较难以分析有效信息。

# 构建速度分析:speed-measure-webpack-plugin

通过speed-measure-webpack-plugin插件进行分析,可以很方便的分析出每个 loader和plugin执行时的耗时,其能有效的分析了整个打包的总耗时插件和loader 耗时情况,能有效的提供有效信息。

安装依赖

npm i speed-measure-webpack-plugin -D

对webpack.prod.js进行分析

//webpack.prod.js
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasureWebpackPlugin()

modue.exports = smp.wrap({...})

# 构建体积分析:webpack-bundle-analyzer

通过webpack-bundle-analyzer将打包后项目进行可视化的分析,其中可分析依赖 的第三方模块文件大小业务组件代码大小,进而对大文件分析是否可进行组件 替换或者CDN方式抽取。

安装依赖

npm i webpack-bundle-anaylzer -D

webpack.prod.js添加插件,然后监听8888端口即可得到对于的体积分析。

//webpack.prod.js
const WebpackBundleAnalyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
    plugins: [
        new WebpackBundleAnalyzer()
    ]
}

# 构建速度优化

# 使用更高版本的webpack和node

通过使用更高版本的webpack和node,在实际项目上,通过webpack4的使用也是比webpack3 的构建速度有近40%的提升。
其中V8带来的优化措施有:for of替代forEach、Map和Set替代Object、includes替代indexOf 等。
webpack4中也通过了使用更快的md4 hash算法替代md5;webpack AST直接从loader传递给AST,减少 解析时间;使用字符串方法替代正则表达式等。

# 多进程/多实例构建

# 解析资源

  • 使用HappyPack解析资源
    HappyPack解析原理即每次webpack解析一个模块,HappyPack会将它及它的依赖分配给worker线程。
    其实质是HappyPack会在webpack执行compiler.run后,进行初始化并创建HappyThreadPool线程池, 线程池则会将构建模块的任务进行分配线程,所以一个线程池中会运行多个线程,并对模块或者依赖进行处理, 处理完成后会将资源传说给HappyPack主进程,完成构建。
    How it works

安装happypack依赖

npm i happypack -D

配置webpack

//webpack.prod.js
const Happypack = require('happypack')
module.exports ={
    module: {
        rules: [
            {
                 test: /.js$/,
                 use: [
                    'happypack/loader'
                     /*'babel-loader'*/
                 ]
            },
        ]
    },
    plugins: [
        new Happypack({
           loaders: ['babel-loader']
        })
    ]
}
  • 使用thread-loader解析资源(推荐使用)
    thread-loader是webpack4官方提供的,其原理也是通过每次webpack解析一个模块,thread-loader 将它及它的依赖分配给worker线程实现多线程构建。

安装thread-loader

npm i thread-loader -D

配置webpack

// webpack.prod.js
module.exports = {
    module:{
        rules:[
            {
                test: /.js$/,
                exclude: /node_modules/,
                use: [
                   {
                       loader: 'thread-loader',
                       options: {
                           workers:3
                       }
                    },
                    'babel-loader'
                 ]
             },
        ]
    }
}

# 并行压缩

const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
module.exports = {
    plugins: [
        new ParallelUglifyPlugin({
        UglifyJS:{
            output: {
                beautify: false,
                comments: false
            },
            compress: {
                warnings: false,
                drop_console: true,
                collapse_vars: true,
                reduce_vars: true
            }
        }
        })
    ]
}
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
    plugins: [
        new UglifyJsPlugin({
         uglifyOptions:{
            warnings: false,
            parse: {},
            compress: {},
            mangle:true,
            output: null,
            nameCache: null,
            ie8: false,
            keep_fnames: false
            },
            parallel: true
        })
    ]
}
  • 使用terser-webpack-plugin开启parallel参数(推荐使用)
    terser-webpack-plugin与uglifyjs-webpack-plugin的区别就是terser-webpack-plugin 插件是支持ES6语法压缩,其中,webpack4默认推荐使用该插件。

安装terser-webpack-plugin插件

npm i terser-webpack-plugin -D

配置webpack

//webpack.prod.js
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
    optimization: {
         minimizer: [
             new TerserPlugin({
                  parallel: true
             })
         ]
    }
}

# 分包

  • 设置Externals
    通过将react、react-dom,UI库等基础包通过CDN方式引入,不打入到bundle中,实现基础库的分离, 但是,这样也会由于一个基础库指向一个CDN,则需要配置多个scripts引入导致多次的请求。而如果借助 之前说的splitChunks实现分包也会使构建项目时进行基础包的分析,加大构建时长。
    安装html-webpack-externals-plugin插件
npm i html-webpack-externals-plugin -D

配置webpack

//webpack.prod.js
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin')
module.exports = {
    plugins: [
        new HtmlWebpackExternalsPlugin({
        externals:[
            {
                module:'react',
                entry: 'https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/cjs/react.production.min.js',
                global: 'react',
            },
            {
                module:'react-dom',
                entry: 'https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/cjs/react-dom.production.min.js',
                global: 'ReactDom',
            }
        ]
        })
    ]
}
  • 预编译资源模块:DLLPlugin + DllReferencePlugin(推荐)
    通过将react,react-dom,UI库等基础包和业务基础包打包成一个文件。其中DLLPlugin通过将 多个基础包或者业务基础包提取,并生成一个包文件和manifest.json(对分离包的一个描述),然后 在实际应用中可以借助DllReferencePlugin对manifest.json引用即可实现分离包的关联。

新建webpack.dll.js并配置scripts命令,使用DLLPlugin进行分包

//package.json
{
    "scripts":{
        "dll": "webpack --config webpack.dll.js"
    }
}
//webpack.dll.js
const path = require('path')
const webpack = require('webpack')

module.exports = {
    entry: {
        library: [ // 分离基础包,如果需要分离业务基础包可以配置多个
            'react',
            'react-dom'
        ]
    },
    output: {
        filename: '[name]_[chunkhash:8].dll.js',
        path: path.join(__dirname, './build/library'), //避免build时候清理dist目录
        library: '[name]'
    },
    plugins: [
        new webpack.DllPlugin({ //提供manifest引用
            name: '[name]_[hash:8]',
            path: path.join(__dirname, './build/library/[name].json')
        })
    ]
}

使用DllReferencePlugin引用manifest.json

//webpack.prod.js
module.exports = {
    plugins: [
        new webpack.DllReferencePlugin({
             manifest: require('./build/library/library.json')
        })
    ]
}

# 缓存

开启构建项目时候的缓存可以提升二次构建的速度。其中,可以通过babel-loader开启缓存 则下次进行babel转换JS、JSX语法时直接读取缓存的内容;在代码压缩阶段可以使用UglifyJsPlugin 或TerserWebpackPlugin开启缓存;通过cache-loader或者hard-source-webpack-plugin 可以提升模块转换阶段的缓存。
对于的缓存内容可以在node_modules下的.cache目录。

//webpack.prod.js
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')

module.exports = {
    module: {
        rules:[
            {
                test: /.js$/,
                use:[
                    {
                       loader: 'thread-loader',
                       options: {
                           workers:3
                         }
                     },
                     'babel-loader?cacheDirectory=true' //开启babel缓存
                ]
            }
        ]
    },
    plugins: [
       new HardSourceWebpackPlugin()  
    ],
    optimization:{
        minimizer: [
          new TerserPlugin({
              parallel: true,
              cache: true // 开启缓存
          })
        ]
    }
}

# 缩小构建目标

  • 少构建模块
    对于一些第三方的模块,我们可以不需要进行进一步的解析,比如babel-loader可以不用解析node_modules 的一些例如UI库等一些第三方包,因为其质量也是有了保证的。
module.exports = {
    module:{
     rules:[
         test: /.js$/,
         loader: 'happypack/loader',
         exclude: 'node_modules' //include:path.resolve('src')
     ]   
    }
}
  • 减少文件搜索范围
    优化resolve.modules配置,较少模块搜索层级;优化resolve.mainFields配置,优化入口配置; 优化resolve.extensions配置,优化查找文件对于的后缀;合理使用alias等。
//webpack.prod.js
module.exports ={
    resolve: {
        alias: {
            'react': path.resolve(__dirname,'./node_modules/react/umd/react.production.min.js'),
               'react-dom': path.resolve(__dirname,'./node_modules/react/umd/react-dom.production.min.js')
        },
        extensions: ['.js'],
        mainFields: ['main']
    }
}

# 构建体积优化

# tree shaking 摇树优化

前面已经有说到过,webpack4设置mode:production默认开启tree shaking,通过 将模块中被使用到的方法打包到bundle文件,而没有用到的方法则在代码压缩阶段被擦除。 并且方法必须是ES6的语法。

# 删除无效的CSS

  • PurifyCSS
    遍历代码,识别已经用到的CSS class,通过对用到和没有使用到的class标记。 目前purifycss-webpack已停止更新,需替换为在webpack4中目前需要通过 purgecss-webpack-plugin结合mini-css-extract-plugin配合使用。

安装purgecss-webpack-plugin

npm i purgecss-webpack-plugin -D

配置webpack

//webpack.prod.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const PurgecssPlugin = 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 PurgecssPlugin({
              path: glob.sync(`${PATHS.src}/**/*`, {nodir:true}) // 绝对路径
         }),
    ]
}
  • uncss
    其中要求HTML需要通过jsdom加载并且所有的样式通过PostCSS解析,这样才能通过 document.querySelector来识别在html文件里面不存在的选择器。

# 图片压缩

在项目上,js和css资源其实对比使用到的图片资源来说是小得多的,如果项目上存在大量 的图片的话,通过对图片的压缩来实现项目体积的优化的权重是比较高的。
基于Node的imagemin,可以通过配置image-webpack-loader来实现,其中,Imagemin可提供定制选项,例如可以 引入更多第三方优化插件,pngquant等,并且也支持多种图片格式压缩。

安装image-webpack-loader

npm i image-webpack-loader -D

配置webpack

module.exports ={
    module:{
        rules:[
            {
               test: /.(png|jpg|gif|jpeg)$/,
               use: [
                  {
                      loader: 'file-loader',
                      options: {
                          name: '[name]_[hash:8].[ext]'
                      }
                  },
                  {
                      loader: 'image-webpack-loader',
                      options: {
                          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
                            }
                      }
                  },
               ]
            },
        ]
    }
}

# 设置动态Polyfill

对于一些es6的语法,不同浏览器可能需要不同的兼容处理,而polyfill就为我们提供了 兼容的方法,但是,完整的polyfill需要处理的兼容语法比较多,这就会导致构建的 体积比较大。
目前常用的polyfill方案:

方案 优点 缺点
babel-polyfill React官方推荐 包体积200k+,难以单独抽离Map、Set;项目里react是单独引用的cdn,如果要用到它,需要单独构建一份放到react前加载
babel-plugin-transform-runtime 只能polyfill用到类或者方法,体积小 不能polyfill原型上的方法,不适合业务项目复杂的开发环境
自建Map、Set的polyfill 定制化高,体积小 重复造轮子,需要维护;即使体积小,需全部加载
polyfill-service 只给用户所需要的polyfill,社群维护 国内部分浏览器可能无法识别(可通过降级返回所需全部polyfill)

推荐使用polyfill-service实现,其通过识别User Agent,下发不同的polyfill进行按需引用。
可通过polyfill.io官方提供的服务:

<script src="https://cdn.polyfill.io/v3/polyfill.min.js"></script>

线上代码可参考:https://github.com/PCAaron/blogCode/tree/master/webpack/webpack-improveMore